Compare commits
	
		
			13 Commits
		
	
	
		
			jude/restr
			...
			jude/docke
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a2df5b9e36 | ||
| 
						 | 
					e98cb67f5f | ||
| 
						 | 
					1d8fd39d13 | ||
| 
						 | 
					91310d47d3 | ||
| 
						 | 
					5ae4baa2a6 | ||
| 
						 | 
					6884adc5b2 | ||
| 
						 | 
					6ade91e11b | ||
| 20f0fb1c20 | |||
| 
						 | 
					4d14365f2b | ||
| 
						 | 
					0f4df703eb | ||
| 
						 | 
					a9edcec43c | ||
| 
						 | 
					cc5f6d9d55 | ||
| 
						 | 
					761d545496 | 
							
								
								
									
										12
									
								
								.sqlx/query-0402e16b1ec89a96d893d43f6b40500ccbde3c619116a702c87954df49898e23.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.sqlx/query-0402e16b1ec89a96d893d43f6b40500ccbde3c619116a702c87954df49898e23.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
 | 
					  "query": "DELETE FROM patreon_link WHERE user_id = ?",
 | 
				
			||||||
 | 
					  "describe": {
 | 
				
			||||||
 | 
					    "columns": [],
 | 
				
			||||||
 | 
					    "parameters": {
 | 
				
			||||||
 | 
					      "Right": 1
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nullable": []
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "hash": "0402e16b1ec89a96d893d43f6b40500ccbde3c619116a702c87954df49898e23"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "\n                        UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)\n                        ",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 1
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": []
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "09f6269e5df3acc01e8e2660532b5fcab21c0cd4fd126b580176d24578932d7e"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										24
									
								
								.sqlx/query-19bc60a2ff67ce6e169985a76405af51d7d16d4d7b84d1c239de5af79da93268.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.sqlx/query-19bc60a2ff67ce6e169985a76405af51d7d16d4d7b84d1c239de5af79da93268.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
 | 
					  "query": "\n                SELECT IFNULL(timezone, 'UTC') AS timezone\n                FROM users\n                WHERE timezone IS NOT NULL\n                GROUP BY timezone\n                ORDER BY COUNT(timezone) DESC\n                LIMIT 21\n                ",
 | 
				
			||||||
 | 
					  "describe": {
 | 
				
			||||||
 | 
					    "columns": [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "ordinal": 0,
 | 
				
			||||||
 | 
					        "name": "timezone",
 | 
				
			||||||
 | 
					        "type_info": {
 | 
				
			||||||
 | 
					          "type": "VarString",
 | 
				
			||||||
 | 
					          "flags": "NOT_NULL",
 | 
				
			||||||
 | 
					          "max_size": 128
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "parameters": {
 | 
				
			||||||
 | 
					      "Right": 0
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nullable": [
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "hash": "19bc60a2ff67ce6e169985a76405af51d7d16d4d7b84d1c239de5af79da93268"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								.sqlx/query-2d780695fe98347ea4ab2cb745462f0a9c55cf913c71d4d822b91958f4f8a729.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.sqlx/query-2d780695fe98347ea4ab2cb745462f0a9c55cf913c71d4d822b91958f4f8a729.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
 | 
					  "query": "\n                DELETE FROM reminders\n                WHERE `utc_time` < NOW() - INTERVAL ? DAY\n                    AND status != 'pending'\n                ORDER BY `utc_time`\n                LIMIT 1000\n                ",
 | 
				
			||||||
 | 
					  "describe": {
 | 
				
			||||||
 | 
					    "columns": [],
 | 
				
			||||||
 | 
					    "parameters": {
 | 
				
			||||||
 | 
					      "Right": 1
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nullable": []
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "hash": "2d780695fe98347ea4ab2cb745462f0a9c55cf913c71d4d822b91958f4f8a729"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "\n                UPDATE reminders\n                INNER JOIN `channels`\n                    ON `channels`.id = reminders.channel_id\n                SET reminders.`utc_time` = reminders.`utc_time` + ?\n                WHERE channels.`channel` = ?\n                ",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 2
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": []
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "2db489e076c93a5a2baf2dd48eb3278d68296aea93097a642e2bbb5112d51fe8"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,134 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "\n                        SELECT\n                            reminders.id,\n                            reminders.uid,\n                            channels.channel,\n                            reminders.utc_time,\n                            reminders.interval_seconds,\n                            reminders.interval_days,\n                            reminders.interval_months,\n                            reminders.expires,\n                            reminders.enabled,\n                            reminders.content,\n                            reminders.embed_description,\n                            reminders.set_by\n                        FROM\n                            reminders\n                        LEFT JOIN\n                            channels\n                        ON\n                            channels.id = reminders.channel_id\n                        WHERE\n                            `status` = 'pending' AND\n                            FIND_IN_SET(channels.channel, ?)\n                        ",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 0,
 | 
					 | 
				
			||||||
        "name": "id",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 1,
 | 
					 | 
				
			||||||
        "name": "uid",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
 | 
					 | 
				
			||||||
          "max_size": 256
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 2,
 | 
					 | 
				
			||||||
        "name": "channel",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "LongLong",
 | 
					 | 
				
			||||||
          "flags": "UNIQUE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
 | 
					 | 
				
			||||||
          "max_size": 20
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 3,
 | 
					 | 
				
			||||||
        "name": "utc_time",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Datetime",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
 | 
					 | 
				
			||||||
          "max_size": 19
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 4,
 | 
					 | 
				
			||||||
        "name": "interval_seconds",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 5,
 | 
					 | 
				
			||||||
        "name": "interval_days",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 6,
 | 
					 | 
				
			||||||
        "name": "interval_months",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 7,
 | 
					 | 
				
			||||||
        "name": "expires",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Datetime",
 | 
					 | 
				
			||||||
          "flags": "BINARY",
 | 
					 | 
				
			||||||
          "max_size": 19
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 8,
 | 
					 | 
				
			||||||
        "name": "enabled",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Tiny",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 9,
 | 
					 | 
				
			||||||
        "name": "content",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 10,
 | 
					 | 
				
			||||||
        "name": "embed_description",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 11,
 | 
					 | 
				
			||||||
        "name": "set_by",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "LongLong",
 | 
					 | 
				
			||||||
          "flags": "MULTIPLE_KEY | UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 20
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 1
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": [
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "3695f95cea95c075b2b3becdf1b5d75bf1ccace3b9a176086faa4ad76c0a0fbd"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "\n                UPDATE reminders\n                INNER JOIN `channels`\n                    ON `channels`.id = reminders.channel_id\n                SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND)\n                WHERE FIND_IN_SET(channels.`channel`, ?)\n                ",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 2
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": []
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "65496ff85dd92b5aaf12519628afdc16ca7d70131744c9c53880dc56b92991d9"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "\n                            DELETE FROM todos WHERE FIND_IN_SET(id, ?)\n                            ",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 1
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": []
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "6e00a27fa770d1aa8cac48cd8878e712ef536c67eeb4fb9a4a801459ada35715"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										24
									
								
								.sqlx/query-92cdd6af01e398b22112ffe88b9ff63d9cc61faaf0dee9eda974efbc8bf84173.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.sqlx/query-92cdd6af01e398b22112ffe88b9ff63d9cc61faaf0dee9eda974efbc8bf84173.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
 | 
					  "query": "SELECT user_id FROM patreon_link WHERE guild_id = ?",
 | 
				
			||||||
 | 
					  "describe": {
 | 
				
			||||||
 | 
					    "columns": [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "ordinal": 0,
 | 
				
			||||||
 | 
					        "name": "user_id",
 | 
				
			||||||
 | 
					        "type_info": {
 | 
				
			||||||
 | 
					          "type": "LongLong",
 | 
				
			||||||
 | 
					          "flags": "NOT_NULL | PRIMARY_KEY | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
 | 
				
			||||||
 | 
					          "max_size": 20
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "parameters": {
 | 
				
			||||||
 | 
					      "Right": 1
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nullable": [
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "hash": "92cdd6af01e398b22112ffe88b9ff63d9cc61faaf0dee9eda974efbc8bf84173"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								.sqlx/query-93897198be27266cd9de90063ee67594cf65c1216c9b9787fc96cd8ffcc1cdef.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.sqlx/query-93897198be27266cd9de90063ee67594cf65c1216c9b9787fc96cd8ffcc1cdef.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
 | 
					  "query": "\n                    DELETE FROM reminders\n                    WHERE `utc_time` < NOW() - INTERVAL ? DAY\n                    ORDER BY `utc_time`\n                    LIMIT 1000\n                    ",
 | 
				
			||||||
 | 
					  "describe": {
 | 
				
			||||||
 | 
					    "columns": [],
 | 
				
			||||||
 | 
					    "parameters": {
 | 
				
			||||||
 | 
					      "Right": 1
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nullable": []
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "hash": "93897198be27266cd9de90063ee67594cf65c1216c9b9787fc96cd8ffcc1cdef"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								.sqlx/query-9b871f08d294555453696808185c6d29d4753619fbee6295a053cefaa9dcc0ae.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.sqlx/query-9b871f08d294555453696808185c6d29d4753619fbee6295a053cefaa9dcc0ae.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
 | 
					  "query": "\n                    UPDATE reminders\n                    INNER JOIN `channels`\n                        ON `channels`.id = reminders.channel_id\n                    SET reminders.`utc_time` = reminders.`utc_time` + ?\n                    WHERE channels.`channel` = ?\n                    ",
 | 
				
			||||||
 | 
					  "describe": {
 | 
				
			||||||
 | 
					    "columns": [],
 | 
				
			||||||
 | 
					    "parameters": {
 | 
				
			||||||
 | 
					      "Right": 2
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nullable": []
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "hash": "9b871f08d294555453696808185c6d29d4753619fbee6295a053cefaa9dcc0ae"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								.sqlx/query-a647934dc5485cfbf430c77b71f7b181f888b0961d5274621e5e1dd76417080e.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.sqlx/query-a647934dc5485cfbf430c77b71f7b181f888b0961d5274621e5e1dd76417080e.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
 | 
					  "query": "INSERT INTO patreon_link (user_id, guild_id, linked_at) VALUES (?, ?, NOW())\n             ON DUPLICATE KEY UPDATE guild_id = ?",
 | 
				
			||||||
 | 
					  "describe": {
 | 
				
			||||||
 | 
					    "columns": [],
 | 
				
			||||||
 | 
					    "parameters": {
 | 
				
			||||||
 | 
					      "Right": 3
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nullable": []
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "hash": "a647934dc5485cfbf430c77b71f7b181f888b0961d5274621e5e1dd76417080e"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,264 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "\n                SELECT\n                 reminders.attachment_name,\n                 reminders.avatar,\n                 channels.channel,\n                 reminders.content,\n                 reminders.embed_author,\n                 reminders.embed_author_url,\n                 reminders.embed_color,\n                 reminders.embed_description,\n                 reminders.embed_footer,\n                 reminders.embed_footer_url,\n                 reminders.embed_image_url,\n                 reminders.embed_thumbnail_url,\n                 reminders.embed_title,\n                 IFNULL(reminders.embed_fields, '[]') AS embed_fields,\n                 reminders.enabled,\n                 reminders.expires,\n                 reminders.interval_seconds,\n                 reminders.interval_days,\n                 reminders.interval_months,\n                 reminders.name,\n                 reminders.restartable,\n                 reminders.tts,\n                 reminders.uid,\n                 reminders.username,\n                 reminders.utc_time\n                FROM reminders\n                INNER JOIN channels ON channels.id = reminders.channel_id\n                WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 0,
 | 
					 | 
				
			||||||
        "name": "attachment_name",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 1040
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 1,
 | 
					 | 
				
			||||||
        "name": "avatar",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 2,
 | 
					 | 
				
			||||||
        "name": "channel",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "LongLong",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
 | 
					 | 
				
			||||||
          "max_size": 20
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 3,
 | 
					 | 
				
			||||||
        "name": "content",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 4,
 | 
					 | 
				
			||||||
        "name": "embed_author",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1024
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 5,
 | 
					 | 
				
			||||||
        "name": "embed_author_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 6,
 | 
					 | 
				
			||||||
        "name": "embed_color",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 7,
 | 
					 | 
				
			||||||
        "name": "embed_description",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 8,
 | 
					 | 
				
			||||||
        "name": "embed_footer",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 9,
 | 
					 | 
				
			||||||
        "name": "embed_footer_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 10,
 | 
					 | 
				
			||||||
        "name": "embed_image_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 11,
 | 
					 | 
				
			||||||
        "name": "embed_thumbnail_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 12,
 | 
					 | 
				
			||||||
        "name": "embed_title",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1024
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 13,
 | 
					 | 
				
			||||||
        "name": "embed_fields",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | BINARY",
 | 
					 | 
				
			||||||
          "max_size": 4294967292
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 14,
 | 
					 | 
				
			||||||
        "name": "enabled",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Tiny",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 15,
 | 
					 | 
				
			||||||
        "name": "expires",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Datetime",
 | 
					 | 
				
			||||||
          "flags": "BINARY",
 | 
					 | 
				
			||||||
          "max_size": 19
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 16,
 | 
					 | 
				
			||||||
        "name": "interval_seconds",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 17,
 | 
					 | 
				
			||||||
        "name": "interval_days",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 18,
 | 
					 | 
				
			||||||
        "name": "interval_months",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 19,
 | 
					 | 
				
			||||||
        "name": "name",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 400
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 20,
 | 
					 | 
				
			||||||
        "name": "restartable",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Tiny",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 21,
 | 
					 | 
				
			||||||
        "name": "tts",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Tiny",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 22,
 | 
					 | 
				
			||||||
        "name": "uid",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
 | 
					 | 
				
			||||||
          "max_size": 256
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 23,
 | 
					 | 
				
			||||||
        "name": "username",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 128
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 24,
 | 
					 | 
				
			||||||
        "name": "utc_time",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Datetime",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
 | 
					 | 
				
			||||||
          "max_size": 19
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 1
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": [
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "af5bf4c6b30ffd316ecebc2dd53554e41f0d4f40cad63736930d20cb18159b38"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,24 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "\n        SELECT IFNULL(timezone, 'UTC') AS timezone\n        FROM users\n        WHERE timezone IS NOT NULL\n        GROUP BY timezone\n        ORDER BY COUNT(timezone) DESC\n        LIMIT 21\n        ",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 0,
 | 
					 | 
				
			||||||
        "name": "timezone",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 128
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 0
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": [
 | 
					 | 
				
			||||||
      false
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "bbdd4bd7ebffb97efab8ba7e829159e104615260929341ec0e961b4d5cd6ca0c"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "db_name": "MySQL",
 | 
					  "db_name": "MySQL",
 | 
				
			||||||
  "query": "\n            SELECT\n                reminders.id,\n                reminders.uid,\n                channels.channel,\n                reminders.utc_time,\n                reminders.interval_seconds,\n                reminders.interval_days,\n                reminders.interval_months,\n                reminders.expires,\n                reminders.enabled,\n                reminders.content,\n                reminders.embed_description,\n                reminders.set_by\n            FROM\n                reminders\n            INNER JOIN\n                channels\n            ON\n                reminders.channel_id = channels.id\n            WHERE\n                `status` = 'pending' AND\n                channels.channel = ? AND\n                FIND_IN_SET(reminders.enabled, ?)\n            ORDER BY\n                reminders.utc_time\n            ",
 | 
					  "query": "\n            SELECT\n                reminders.id,\n                reminders.uid,\n                channels.channel,\n                reminders.utc_time,\n                reminders.interval_seconds,\n                reminders.interval_days,\n                reminders.interval_months,\n                reminders.expires,\n                reminders.enabled,\n                reminders.content,\n                reminders.embed_description,\n                reminders.set_by\n            FROM\n                reminders\n            INNER JOIN\n                channels\n            ON\n                reminders.channel_id = channels.id\n            WHERE\n                `status` = 'pending' AND\n                channels.channel = ? AND\n                reminders.enabled >= ?\n            ORDER BY\n                reminders.utc_time\n            ",
 | 
				
			||||||
  "describe": {
 | 
					  "describe": {
 | 
				
			||||||
    "columns": [
 | 
					    "columns": [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
@@ -130,5 +130,5 @@
 | 
				
			|||||||
      true
 | 
					      true
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "hash": "d60ea641070dbd882cd53878fa109d08bd3f65e0da8c263e78fe0b200228bc2b"
 | 
					  "hash": "d2921961627fef0e12892dbbcd4b891e58c0e52c20897aab3d95365774c01bda"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,264 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "db_name": "MySQL",
 | 
					 | 
				
			||||||
  "query": "SELECT\n                 reminders.attachment,\n                 reminders.attachment_name,\n                 reminders.avatar,\n                 CONCAT('#', channels.channel) AS channel,\n                 reminders.content,\n                 reminders.embed_author,\n                 reminders.embed_author_url,\n                 reminders.embed_color,\n                 reminders.embed_description,\n                 reminders.embed_footer,\n                 reminders.embed_footer_url,\n                 reminders.embed_image_url,\n                 reminders.embed_thumbnail_url,\n                 reminders.embed_title,\n                 reminders.embed_fields,\n                 reminders.enabled,\n                 reminders.expires,\n                 reminders.interval_seconds,\n                 reminders.interval_days,\n                 reminders.interval_months,\n                 reminders.name,\n                 reminders.restartable,\n                 reminders.tts,\n                 reminders.username,\n                 reminders.utc_time\n                FROM reminders\n                LEFT JOIN channels ON channels.id = reminders.channel_id\n                WHERE FIND_IN_SET(channels.channel, ?) AND status = 'pending'",
 | 
					 | 
				
			||||||
  "describe": {
 | 
					 | 
				
			||||||
    "columns": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 0,
 | 
					 | 
				
			||||||
        "name": "attachment",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Blob",
 | 
					 | 
				
			||||||
          "flags": "BLOB | BINARY",
 | 
					 | 
				
			||||||
          "max_size": 16777215
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 1,
 | 
					 | 
				
			||||||
        "name": "attachment_name",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 1040
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 2,
 | 
					 | 
				
			||||||
        "name": "avatar",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 3,
 | 
					 | 
				
			||||||
        "name": "channel",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 84
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 4,
 | 
					 | 
				
			||||||
        "name": "content",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 5,
 | 
					 | 
				
			||||||
        "name": "embed_author",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1024
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 6,
 | 
					 | 
				
			||||||
        "name": "embed_author_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 7,
 | 
					 | 
				
			||||||
        "name": "embed_color",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 8,
 | 
					 | 
				
			||||||
        "name": "embed_description",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 9,
 | 
					 | 
				
			||||||
        "name": "embed_footer",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 8192
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 10,
 | 
					 | 
				
			||||||
        "name": "embed_footer_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 11,
 | 
					 | 
				
			||||||
        "name": "embed_image_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 12,
 | 
					 | 
				
			||||||
        "name": "embed_thumbnail_url",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 2048
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 13,
 | 
					 | 
				
			||||||
        "name": "embed_title",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1024
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 14,
 | 
					 | 
				
			||||||
        "name": "embed_fields",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Json",
 | 
					 | 
				
			||||||
          "flags": "BLOB | BINARY",
 | 
					 | 
				
			||||||
          "max_size": 4294967295
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 15,
 | 
					 | 
				
			||||||
        "name": "enabled",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Tiny",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 16,
 | 
					 | 
				
			||||||
        "name": "expires",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Datetime",
 | 
					 | 
				
			||||||
          "flags": "BINARY",
 | 
					 | 
				
			||||||
          "max_size": 19
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 17,
 | 
					 | 
				
			||||||
        "name": "interval_seconds",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 18,
 | 
					 | 
				
			||||||
        "name": "interval_days",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 19,
 | 
					 | 
				
			||||||
        "name": "interval_months",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Long",
 | 
					 | 
				
			||||||
          "flags": "UNSIGNED",
 | 
					 | 
				
			||||||
          "max_size": 10
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 20,
 | 
					 | 
				
			||||||
        "name": "name",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 400
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 21,
 | 
					 | 
				
			||||||
        "name": "restartable",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Tiny",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 22,
 | 
					 | 
				
			||||||
        "name": "tts",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Tiny",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL",
 | 
					 | 
				
			||||||
          "max_size": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 23,
 | 
					 | 
				
			||||||
        "name": "username",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "VarString",
 | 
					 | 
				
			||||||
          "flags": "",
 | 
					 | 
				
			||||||
          "max_size": 128
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "ordinal": 24,
 | 
					 | 
				
			||||||
        "name": "utc_time",
 | 
					 | 
				
			||||||
        "type_info": {
 | 
					 | 
				
			||||||
          "type": "Datetime",
 | 
					 | 
				
			||||||
          "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
 | 
					 | 
				
			||||||
          "max_size": 19
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "parameters": {
 | 
					 | 
				
			||||||
      "Right": 1
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "nullable": [
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      false,
 | 
					 | 
				
			||||||
      true,
 | 
					 | 
				
			||||||
      false
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "hash": "db69def9391283efb9bf915223d7d6b2d169203b7dc45481a509137f7590d9a6"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										14
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
# This file is automatically @generated by Cargo.
 | 
					# This file is automatically @generated by Cargo.
 | 
				
			||||||
# It is not intended for manual editing.
 | 
					# It is not intended for manual editing.
 | 
				
			||||||
version = 3
 | 
					version = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "addr2line"
 | 
					name = "addr2line"
 | 
				
			||||||
@@ -524,6 +524,15 @@ dependencies = [
 | 
				
			|||||||
 "cfg-if",
 | 
					 "cfg-if",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "cron-parser"
 | 
				
			||||||
 | 
					version = "0.10.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "baa5650eabdaa360e2c240c2a5f544f10185b439cd76d748e44e3f28128a016b"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "crossbeam-channel"
 | 
					name = "crossbeam-channel"
 | 
				
			||||||
version = "0.5.13"
 | 
					version = "0.5.13"
 | 
				
			||||||
@@ -2614,11 +2623,12 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "reminder-rs"
 | 
					name = "reminder-rs"
 | 
				
			||||||
version = "1.7.37"
 | 
					version = "1.7.41"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "base64 0.22.1",
 | 
					 "base64 0.22.1",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "chrono-tz",
 | 
					 "chrono-tz",
 | 
				
			||||||
 | 
					 "cron-parser",
 | 
				
			||||||
 "csv",
 | 
					 "csv",
 | 
				
			||||||
 "dotenv",
 | 
					 "dotenv",
 | 
				
			||||||
 "env_logger",
 | 
					 "env_logger",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder-rs"
 | 
					name = "reminder-rs"
 | 
				
			||||||
version = "1.7.37"
 | 
					version = "1.7.41"
 | 
				
			||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
					authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
license = "AGPL-3.0 only"
 | 
					license = "AGPL-3.0 only"
 | 
				
			||||||
@@ -35,6 +35,7 @@ serenity = { version = "0.12", default-features = false, features = ["builder",
 | 
				
			|||||||
oauth2 = "4"
 | 
					oauth2 = "4"
 | 
				
			||||||
csv = "1.2"
 | 
					csv = "1.2"
 | 
				
			||||||
sd-notify = "0.4.1"
 | 
					sd-notify = "0.4.1"
 | 
				
			||||||
 | 
					cron-parser = "0.10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.extract_derive]
 | 
					[dependencies.extract_derive]
 | 
				
			||||||
path = "extract_derive"
 | 
					path = "extract_derive"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,4 +34,5 @@ COPY ./dp.py ./
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
RUN cargo install --path .
 | 
					RUN cargo install --path .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					EXPOSE 18920
 | 
				
			||||||
CMD ["reminder-rs"]
 | 
					CMD ["reminder-rs"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								compose.yaml
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								compose.yaml
									
									
									
									
									
								
							@@ -1,21 +1,41 @@
 | 
				
			|||||||
version: '3.3'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
services:
 | 
					services:
 | 
				
			||||||
    bot:
 | 
					 | 
				
			||||||
        build: ./Containerfile.run
 | 
					 | 
				
			||||||
        image: reminder-rs-run
 | 
					 | 
				
			||||||
        restart: always
 | 
					 | 
				
			||||||
        expose:
 | 
					 | 
				
			||||||
            - '80'
 | 
					 | 
				
			||||||
  database:
 | 
					  database:
 | 
				
			||||||
    image: mysql:8.0
 | 
					    image: mysql:8.0
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
 | 
					    command: --log-bin-trust-function-creators=1
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
            MYSQL_DATABASE: 'reminders'
 | 
					      MYSQL_ROOT_PASSWORD: root
 | 
				
			||||||
            MYSQL_USER: 'reminder-bot'
 | 
					      MYSQL_DATABASE: reminders
 | 
				
			||||||
        ports:
 | 
					      MYSQL_USER: reminder-bot
 | 
				
			||||||
            - '3306:3306'
 | 
					      MYSQL_PASSWORD: password
 | 
				
			||||||
        expose:
 | 
					 | 
				
			||||||
            - '3306'
 | 
					 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - reminders:/var/lib/mysql
 | 
					      - reminders:/var/lib/mysql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bot:
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: Containerfile.run
 | 
				
			||||||
 | 
					    image: reminder-rs-run
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    depends_on:
 | 
				
			||||||
 | 
					      - database
 | 
				
			||||||
 | 
					    env_file:
 | 
				
			||||||
 | 
					      - .env
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      DATABASE_URL: "mysql://reminder-bot:password@database/reminders"
 | 
				
			||||||
 | 
					      DISCORD_TOKEN:
 | 
				
			||||||
 | 
					      PATREON_GUILD_ID:
 | 
				
			||||||
 | 
					      PATREON_ROLE_ID:
 | 
				
			||||||
 | 
					      LOCAL_TIMEZONE:
 | 
				
			||||||
 | 
					      MIN_INTERVAL:
 | 
				
			||||||
 | 
					      SECRET_KEY:
 | 
				
			||||||
 | 
					      REMIND_INTERVAL:
 | 
				
			||||||
 | 
					      OAUTH2_DISCORD_CALLBACK:
 | 
				
			||||||
 | 
					      OAUTH2_CLIENT_ID:
 | 
				
			||||||
 | 
					      OAUTH2_CLIENT_SECRET:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - 18920:18920
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					volumes:
 | 
				
			||||||
 | 
					  reminders:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12676
									
								
								gb-ipv4.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12676
									
								
								gb-ipv4.csv
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,83 +0,0 @@
 | 
				
			|||||||
-- Drop all old tables
 | 
					 | 
				
			||||||
DROP TABLE IF EXISTS users_old;
 | 
					 | 
				
			||||||
DROP TABLE IF EXISTS messages;
 | 
					 | 
				
			||||||
DROP TABLE IF EXISTS embeds;
 | 
					 | 
				
			||||||
DROP TABLE IF EXISTS embed_fields;
 | 
					 | 
				
			||||||
DROP TABLE IF EXISTS command_aliases;
 | 
					 | 
				
			||||||
DROP TABLE IF EXISTS macro;
 | 
					 | 
				
			||||||
DROP TABLE IF EXISTS roles;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SET FOREIGN_KEY_CHECKS=0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Drop columns from channels that are no longer used
 | 
					 | 
				
			||||||
ALTER TABLE channels DROP COLUMN `name`;
 | 
					 | 
				
			||||||
ALTER TABLE channels DROP COLUMN `blacklisted`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Drop columns from guilds table that are no longer used and rebuild table
 | 
					 | 
				
			||||||
CREATE TABLE guilds_new (
 | 
					 | 
				
			||||||
    id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
 | 
					 | 
				
			||||||
    ephemeral_confirmations BOOLEAN NOT NULL DEFAULT 0
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
INSERT INTO guilds_new (id, ephemeral_confirmations) SELECT guild, ephemeral_confirmations FROM guilds;
 | 
					 | 
				
			||||||
RENAME TABLE guilds TO guilds_old;
 | 
					 | 
				
			||||||
RENAME TABLE guilds_new TO guilds;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Update fk on channels to point at new guild table
 | 
					 | 
				
			||||||
ALTER TABLE channels
 | 
					 | 
				
			||||||
    DROP FOREIGN KEY `channels_ibfk_1`,
 | 
					 | 
				
			||||||
    MODIFY COLUMN guild_id BIGINT UNSIGNED,
 | 
					 | 
				
			||||||
    ADD FOREIGN KEY `fk_guild_id` (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES `guilds` (`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
UPDATE channels SET guild_id = (SELECT guild FROM guilds_old WHERE id = guild_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Rebuild todos table
 | 
					 | 
				
			||||||
CREATE TABLE `todos_new` (
 | 
					 | 
				
			||||||
    `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
 | 
					 | 
				
			||||||
    `guild_id`   BIGINT UNSIGNED DEFAULT NULL,
 | 
					 | 
				
			||||||
    `channel_id` INT UNSIGNED DEFAULT NULL,
 | 
					 | 
				
			||||||
    `user_id`    BIGINT UNSIGNED DEFAULT NULL,
 | 
					 | 
				
			||||||
    `value`      VARCHAR(2000) NOT NULL,
 | 
					 | 
				
			||||||
    PRIMARY KEY (`id`),
 | 
					 | 
				
			||||||
    FOREIGN KEY `fk_channel_id` (`channel_id`)
 | 
					 | 
				
			||||||
        REFERENCES `channels` (`id`)
 | 
					 | 
				
			||||||
        ON DELETE SET NULL
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE,
 | 
					 | 
				
			||||||
    FOREIGN KEY `fk_guild_id` (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES `guilds` (`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE,
 | 
					 | 
				
			||||||
    FOREIGN KEY `fk_user_id` (`user_id`)
 | 
					 | 
				
			||||||
        REFERENCES `users` (`id`)
 | 
					 | 
				
			||||||
        ON DELETE SET NULL
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
INSERT INTO todos_new (id, guild_id, channel_id, user_id, value) SELECT id, (SELECT guild FROM guilds_old WHERE id = guild_id), channel_id, user_id, value FROM todos;
 | 
					 | 
				
			||||||
RENAME TABLE todos TO todos_old;
 | 
					 | 
				
			||||||
RENAME TABLE todos_new TO todos;
 | 
					 | 
				
			||||||
DROP TABLE todos_old;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Update fk on reminder_template to point at new guild table
 | 
					 | 
				
			||||||
ALTER TABLE reminder_template
 | 
					 | 
				
			||||||
    DROP FOREIGN KEY `reminder_template_ibfk_1`,
 | 
					 | 
				
			||||||
    MODIFY COLUMN guild_id BIGINT UNSIGNED NOT NULL,
 | 
					 | 
				
			||||||
    ADD FOREIGN KEY `fk_guild_id` (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES `guilds` (`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
UPDATE reminder_template SET guild_id = (SELECT guild FROM guilds_old WHERE id = guild_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Update fk on command_macro to point at new guild table
 | 
					 | 
				
			||||||
ALTER TABLE command_macro
 | 
					 | 
				
			||||||
    DROP FOREIGN KEY `command_macro_ibfk_1`,
 | 
					 | 
				
			||||||
    MODIFY COLUMN guild_id BIGINT UNSIGNED NOT NULL,
 | 
					 | 
				
			||||||
    ADD FOREIGN KEY `fk_guild_id` (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES `guilds` (`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
UPDATE command_macro SET guild_id = (SELECT guild FROM guilds_old WHERE id = guild_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DROP TABLE guilds_old;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SET FOREIGN_KEY_CHECKS=1;
 | 
					 | 
				
			||||||
							
								
								
									
										11
									
								
								migrations/20250924203400_patreon_link.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								migrations/20250924203400_patreon_link.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					CREATE TABLE patreon_link
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					    user_id      BIGINT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    guild_id     BIGINT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    linked_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (user_id),
 | 
				
			||||||
 | 
					    INDEX `idx_user_id` (user_id),
 | 
				
			||||||
 | 
					    INDEX `idx_guild_id` (guild_id),
 | 
				
			||||||
 | 
					    INDEX `idx_linked_at` (linked_at)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
 | 
				
			|||||||
use chrono_tz::TZ_VARIANTS;
 | 
					use chrono_tz::TZ_VARIANTS;
 | 
				
			||||||
use poise::serenity_prelude::AutocompleteChoice;
 | 
					use poise::serenity_prelude::AutocompleteChoice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::time_parser::cron_next_timestamp;
 | 
				
			||||||
use crate::{models::CtxData, time_parser::natural_parser, Context};
 | 
					use crate::{models::CtxData, time_parser::natural_parser, Context};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
 | 
					pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
 | 
				
			||||||
@@ -42,7 +43,13 @@ pub async fn time_hint_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Auto
 | 
				
			|||||||
    if partial.is_empty() {
 | 
					    if partial.is_empty() {
 | 
				
			||||||
        vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
 | 
					        vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        match natural_parser(partial, &ctx.timezone().await.to_string()).await {
 | 
					        let timezone = ctx.timezone().await;
 | 
				
			||||||
 | 
					        let timestamp = match cron_next_timestamp(partial, timezone) {
 | 
				
			||||||
 | 
					            Some(ts) => Some(ts),
 | 
				
			||||||
 | 
					            None => natural_parser(partial, &timezone.to_string()).await,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match timestamp {
 | 
				
			||||||
            Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
 | 
					            Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
 | 
				
			||||||
                Ok(now) => {
 | 
					                Ok(now) => {
 | 
				
			||||||
                    let diff = timestamp - now.as_secs() as i64;
 | 
					                    let diff = timestamp - now.as_secs() as i64;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -75,8 +75,8 @@ Please select a unique name for your macro.",
 | 
				
			|||||||
                    CreateEmbed::new()
 | 
					                    CreateEmbed::new()
 | 
				
			||||||
                        .title("Macro Recording Started")
 | 
					                        .title("Macro Recording Started")
 | 
				
			||||||
                        .description(
 | 
					                        .description(
 | 
				
			||||||
                            "Run up to 5 commands, or type `/macro finish` to stop at any point.
 | 
					                            "Run up to 5 commands to record in this macro. Use `/macro finish` to stop recording at any point.
 | 
				
			||||||
Any commands ran as part of recording will be inconsequential",
 | 
					Any commands performed during recording won't take any actual action- they are only captured for the macro.",
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .color(*THEME_COLOR),
 | 
					                        .color(*THEME_COLOR),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,8 +12,6 @@ pub mod dashboard;
 | 
				
			|||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
pub mod delete;
 | 
					pub mod delete;
 | 
				
			||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
pub mod donate;
 | 
					 | 
				
			||||||
#[cfg(not(test))]
 | 
					 | 
				
			||||||
pub mod help;
 | 
					pub mod help;
 | 
				
			||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
pub mod info;
 | 
					pub mod info;
 | 
				
			||||||
@@ -26,6 +24,8 @@ pub mod nudge;
 | 
				
			|||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
pub mod offset;
 | 
					pub mod offset;
 | 
				
			||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
 | 
					pub mod patreon;
 | 
				
			||||||
 | 
					#[cfg(not(test))]
 | 
				
			||||||
pub mod pause;
 | 
					pub mod pause;
 | 
				
			||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
pub mod remind;
 | 
					pub mod remind;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,8 +15,8 @@ impl Recordable for Options {
 | 
				
			|||||||
        let footer = footer(ctx);
 | 
					        let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
 | 
					        ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
 | 
				
			||||||
            .description("Thinking of adding a monthly contribution?
 | 
					            .description("Thinking of subscribing?
 | 
				
			||||||
Click below for my Patreon and official bot server :)
 | 
					Click below for my Patreon and official bot server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**https://www.patreon.com/jellywx/**
 | 
					**https://www.patreon.com/jellywx/**
 | 
				
			||||||
**https://discord.jellywx.com/**
 | 
					**https://discord.jellywx.com/**
 | 
				
			||||||
@@ -26,7 +26,7 @@ With your new rank, you'll be able to:
 | 
				
			|||||||
• Set repeating reminders with `/remind` or the dashboard
 | 
					• Set repeating reminders with `/remind` or the dashboard
 | 
				
			||||||
• Use unlimited uploads on SoundFX
 | 
					• Use unlimited uploads on SoundFX
 | 
				
			||||||
 | 
					
 | 
				
			||||||
(Also, members of servers you __own__ will be able to set repeating reminders via commands)
 | 
					Members of servers you __own__ will be able to set repeating reminders via commands. You can also choose to share your membership with one other server.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Just $2 USD/month!
 | 
					Just $2 USD/month!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -41,8 +41,8 @@ Just $2 USD/month!
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Details on supporting the bot and Patreon benefits
 | 
					/// Show Patreon information
 | 
				
			||||||
#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
 | 
					#[poise::command(slash_command, rename = "info", identifying_name = "patreon_info")]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    (Options {}).run(ctx).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										57
									
								
								src/commands/patreon/link.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/commands/patreon/link.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					use poise::CreateReply;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    utils::{check_user_subscription, Extract, Recordable},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Recordable for Options {
 | 
				
			||||||
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let guild_id = ctx.guild_id().ok_or("This command must be used in a server")?;
 | 
				
			||||||
 | 
					        let user_id = ctx.author().id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check if user has Patreon subscription
 | 
				
			||||||
 | 
					        if !check_user_subscription(ctx, user_id).await {
 | 
				
			||||||
 | 
					            ctx.send(CreateReply::default()
 | 
				
			||||||
 | 
					                .content("❌ You must be a Patreon subscriber to use this command. Use `/patreon info` for more information.")
 | 
				
			||||||
 | 
					                .ephemeral(true)
 | 
				
			||||||
 | 
					            ).await?;
 | 
				
			||||||
 | 
					            return Ok(());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Insert or update the patreon_link entry
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "INSERT INTO patreon_link (user_id, guild_id, linked_at) VALUES (?, ?, NOW())
 | 
				
			||||||
 | 
					             ON DUPLICATE KEY UPDATE guild_id = ?",
 | 
				
			||||||
 | 
					            user_id.get(),
 | 
				
			||||||
 | 
					            guild_id.get(),
 | 
				
			||||||
 | 
					            guild_id.get()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx.send(
 | 
				
			||||||
 | 
					            CreateReply::default()
 | 
				
			||||||
 | 
					                .content("✅ Successfully linked your Patreon subscription to this server!")
 | 
				
			||||||
 | 
					                .ephemeral(true),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Link your Patreon subscription to this server to allow other users Patreon access.
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "link",
 | 
				
			||||||
 | 
					    identifying_name = "patreon_link",
 | 
				
			||||||
 | 
					    guild_only = true
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn link(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/commands/patreon/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/commands/patreon/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					pub mod info;
 | 
				
			||||||
 | 
					pub mod link;
 | 
				
			||||||
 | 
					pub mod unlink;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Manage Patreon subscription features
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
 | 
				
			||||||
 | 
					pub async fn command(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								src/commands/patreon/unlink.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/commands/patreon/unlink.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					use poise::CreateReply;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Recordable for Options {
 | 
				
			||||||
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let user_id = ctx.author().id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Remove the patreon_link entry
 | 
				
			||||||
 | 
					        let result = sqlx::query!("DELETE FROM patreon_link WHERE user_id = ?", user_id.get())
 | 
				
			||||||
 | 
					            .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if result.rows_affected() > 0 {
 | 
				
			||||||
 | 
					            ctx.send(
 | 
				
			||||||
 | 
					                CreateReply::default()
 | 
				
			||||||
 | 
					                    .content("✅ Successfully unlinked your Patreon subscription!")
 | 
				
			||||||
 | 
					                    .ephemeral(true),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            ctx.send(
 | 
				
			||||||
 | 
					                CreateReply::default()
 | 
				
			||||||
 | 
					                    .content("❌ No existing Patreon link found.")
 | 
				
			||||||
 | 
					                    .ephemeral(true),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Unlink your Patreon subscription
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "unlink",
 | 
				
			||||||
 | 
					    identifying_name = "patreon_unlink",
 | 
				
			||||||
 | 
					    guild_only = false
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn unlink(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -21,7 +21,7 @@ impl Recordable for Options {
 | 
				
			|||||||
            "
 | 
					            "
 | 
				
			||||||
            INSERT INTO todos (guild_id, channel_id, value)
 | 
					            INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
            VALUES (
 | 
					            VALUES (
 | 
				
			||||||
                ?,
 | 
					                (SELECT id FROM guilds WHERE guild = ?),
 | 
				
			||||||
                (SELECT id FROM channels WHERE channel = ?),
 | 
					                (SELECT id FROM channels WHERE channel = ?),
 | 
				
			||||||
                ?
 | 
					                ?
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ pub async fn listener(
 | 
				
			|||||||
            if is_new.unwrap_or(false) {
 | 
					            if is_new.unwrap_or(false) {
 | 
				
			||||||
                let guild_id = guild.id.get().to_owned();
 | 
					                let guild_id = guild.id.get().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id)
 | 
					                sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id)
 | 
				
			||||||
                    .execute(&data.database)
 | 
					                    .execute(&data.database)
 | 
				
			||||||
                    .await?;
 | 
					                    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,7 +56,7 @@ To stay up to date on the latest features and fixes, join our [Discord](https://
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        FullEvent::GuildDelete { incomplete, .. } => {
 | 
					        FullEvent::GuildDelete { incomplete, .. } => {
 | 
				
			||||||
            if !incomplete.unavailable {
 | 
					            if !incomplete.unavailable {
 | 
				
			||||||
                let _ = sqlx::query!("DELETE FROM guilds WHERE id = ?", incomplete.id.get())
 | 
					                let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.get())
 | 
				
			||||||
                    .execute(&data.database)
 | 
					                    .execute(&data.database)
 | 
				
			||||||
                    .await;
 | 
					                    .await;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										57
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								src/hooks.rs
									
									
									
									
									
								
							@@ -1,4 +1,6 @@
 | 
				
			|||||||
use poise::{CommandInteractionType, CreateReply};
 | 
					use crate::consts::THEME_COLOR;
 | 
				
			||||||
 | 
					use poise::{serenity_prelude::CreateEmbed, CommandInteractionType, CreateReply};
 | 
				
			||||||
 | 
					use serenity::builder::CreateEmbedFooter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
 | 
					use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,7 +20,18 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
                            .send(
 | 
					                            .send(
 | 
				
			||||||
                                CreateReply::default()
 | 
					                                CreateReply::default()
 | 
				
			||||||
                                    .ephemeral(true)
 | 
					                                    .ephemeral(true)
 | 
				
			||||||
                                    .content(format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS))
 | 
					                                    .embed(CreateEmbed::new()
 | 
				
			||||||
 | 
					                                        .title("💾 Currently recording macro")
 | 
				
			||||||
 | 
					                                        .description(
 | 
				
			||||||
 | 
					                                            format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
 | 
				
			||||||
 | 
					                                        )
 | 
				
			||||||
 | 
					                                        .footer(
 | 
				
			||||||
 | 
					                                            CreateEmbedFooter::new(
 | 
				
			||||||
 | 
					                                                "Any commands performed during recording won't take any actual action- they are only captured for the macro"
 | 
				
			||||||
 | 
					                                            )
 | 
				
			||||||
 | 
					                                        )
 | 
				
			||||||
 | 
					                                        .color(*THEME_COLOR),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                            .await;
 | 
					                            .await;
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
@@ -28,9 +41,19 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                                let _ = ctx
 | 
					                                let _ = ctx
 | 
				
			||||||
                                    .send(
 | 
					                                    .send(
 | 
				
			||||||
                                        CreateReply::default()
 | 
					                                        CreateReply::default().ephemeral(true).embed(
 | 
				
			||||||
                                            .ephemeral(true)
 | 
					                                            CreateEmbed::new()
 | 
				
			||||||
                                            .content("Command recorded to macro"),
 | 
					                                                .title("💾 Currently recording macro")
 | 
				
			||||||
 | 
					                                                .description(
 | 
				
			||||||
 | 
					                                                    "Command recorded. Use `/macro finish` to end recording.",
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                                .footer(
 | 
				
			||||||
 | 
					                                                    CreateEmbedFooter::new(
 | 
				
			||||||
 | 
					                                                        "Any commands performed during recording won't take any actual action- they are only captured for the macro"
 | 
				
			||||||
 | 
					                                                    )
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                                .color(*THEME_COLOR),
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .await;
 | 
					                                    .await;
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
@@ -38,8 +61,18 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
                            None => {
 | 
					                            None => {
 | 
				
			||||||
                                let _ = ctx
 | 
					                                let _ = ctx
 | 
				
			||||||
                                    .send(
 | 
					                                    .send(
 | 
				
			||||||
                                        CreateReply::default().ephemeral(true).content(
 | 
					                                        CreateReply::default().ephemeral(true).embed(
 | 
				
			||||||
                                            "This command is not supported in macros yet.",
 | 
					                                            CreateEmbed::new()
 | 
				
			||||||
 | 
					                                                .title("💾 Currently recording macro")
 | 
				
			||||||
 | 
					                                                .description(
 | 
				
			||||||
 | 
					                                                    "This command is not supported in macros, so it hasn't been recorded. Use `/macro finish` to end recording.",
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                                .footer(
 | 
				
			||||||
 | 
					                                                    CreateEmbedFooter::new(
 | 
				
			||||||
 | 
					                                                        "Any commands performed during recording won't take any actual action- they are only captured for the macro"
 | 
				
			||||||
 | 
					                                                    )
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                                .color(*THEME_COLOR),
 | 
				
			||||||
                                        ),
 | 
					                                        ),
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .await;
 | 
					                                    .await;
 | 
				
			||||||
@@ -74,6 +107,7 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
                return if permissions.send_messages()
 | 
					                return if permissions.send_messages()
 | 
				
			||||||
                    && permissions.embed_links()
 | 
					                    && permissions.embed_links()
 | 
				
			||||||
                    && manage_webhooks
 | 
					                    && manage_webhooks
 | 
				
			||||||
 | 
					                    && permissions.view_channel()
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    true
 | 
					                    true
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
@@ -81,12 +115,13 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
                        .send(CreateReply::default().content(format!(
 | 
					                        .send(CreateReply::default().content(format!(
 | 
				
			||||||
                            "The bot appears to be missing some permissions:
 | 
					                            "The bot appears to be missing some permissions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{}     **View Channels**
 | 
				
			||||||
{}     **Send Message**
 | 
					{}     **Send Message**
 | 
				
			||||||
{}     **Embed Links**
 | 
					{}     **Embed Links**
 | 
				
			||||||
{}     **Manage Webhooks**
 | 
					{}     **Manage Webhooks**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot
 | 
					Please check the bot's roles, and any channel overrides. Alternatively, giving the bot \"Administrator\" will bypass permission checks",
 | 
				
			||||||
\"Administrator\" will bypass permission checks",
 | 
					                            if permissions.view_channel() { "✅" } else { "❌" },
 | 
				
			||||||
                            if permissions.send_messages() { "✅" } else { "❌" },
 | 
					                            if permissions.send_messages() { "✅" } else { "❌" },
 | 
				
			||||||
                            if permissions.embed_links() { "✅" } else { "❌" },
 | 
					                            if permissions.embed_links() { "✅" } else { "❌" },
 | 
				
			||||||
                            if manage_webhooks { "✅" } else { "❌" },
 | 
					                            if manage_webhooks { "✅" } else { "❌" },
 | 
				
			||||||
@@ -100,9 +135,7 @@ Please check the bot's roles, and any channel overrides. Alternatively, giving t
 | 
				
			|||||||
            manage_webhooks
 | 
					            manage_webhooks
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        None => {
 | 
					        None => true,
 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -48,8 +48,8 @@ use crate::test::TestContext;
 | 
				
			|||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{
 | 
					    commands::{
 | 
				
			||||||
        allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
 | 
					        allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
 | 
				
			||||||
        delete, donate, help, info, look, multiline, nudge, offset, pause, remind, settings, timer,
 | 
					        delete, help, info, look, multiline, nudge, offset, patreon, pause, remind, settings,
 | 
				
			||||||
        timezone, todo, webhook,
 | 
					        timer, timezone, todo, webhook,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    event_handlers::listener,
 | 
					    event_handlers::listener,
 | 
				
			||||||
@@ -165,7 +165,14 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
                    help::command(),
 | 
					                    help::command(),
 | 
				
			||||||
                    info::command(),
 | 
					                    info::command(),
 | 
				
			||||||
                    clock::command(),
 | 
					                    clock::command(),
 | 
				
			||||||
                    donate::command(),
 | 
					                    poise::Command {
 | 
				
			||||||
 | 
					                        subcommands: vec![
 | 
				
			||||||
 | 
					                            patreon::link::link(),
 | 
				
			||||||
 | 
					                            patreon::unlink::unlink(),
 | 
				
			||||||
 | 
					                            patreon::info::info(),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        ..patreon::command()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
                    clock_context_menu(),
 | 
					                    clock_context_menu(),
 | 
				
			||||||
                    dashboard::command(),
 | 
					                    dashboard::command(),
 | 
				
			||||||
                    timezone::command(),
 | 
					                    timezone::command(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,9 @@ use crate::{consts::DEFAULT_AVATAR, Error};
 | 
				
			|||||||
pub struct ChannelData {
 | 
					pub struct ChannelData {
 | 
				
			||||||
    pub id: u32,
 | 
					    pub id: u32,
 | 
				
			||||||
    pub channel: u64,
 | 
					    pub channel: u64,
 | 
				
			||||||
 | 
					    pub name: Option<String>,
 | 
				
			||||||
    pub nudge: i16,
 | 
					    pub nudge: i16,
 | 
				
			||||||
 | 
					    pub blacklisted: bool,
 | 
				
			||||||
    pub webhook_id: Option<u64>,
 | 
					    pub webhook_id: Option<u64>,
 | 
				
			||||||
    pub webhook_token: Option<String>,
 | 
					    pub webhook_token: Option<String>,
 | 
				
			||||||
    pub paused: bool,
 | 
					    pub paused: bool,
 | 
				
			||||||
@@ -25,7 +27,7 @@ impl ChannelData {
 | 
				
			|||||||
        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
					        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
            SELECT id, channel, nudge, webhook_id, webhook_token, paused,
 | 
					            SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused,
 | 
				
			||||||
                paused_until
 | 
					                paused_until
 | 
				
			||||||
            FROM channels
 | 
					            FROM channels
 | 
				
			||||||
            WHERE channel = ?
 | 
					            WHERE channel = ?
 | 
				
			||||||
@@ -37,11 +39,15 @@ impl ChannelData {
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            Ok(c)
 | 
					            Ok(c)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            let guild_id = channel.to_owned().guild().map(|g| g.guild_id.get().to_owned());
 | 
					            let props = channel.to_owned().guild().map(|g| (g.guild_id.get().to_owned(), g.name));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let (guild_id, channel_name) =
 | 
				
			||||||
 | 
					                if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            sqlx::query!(
 | 
					            sqlx::query!(
 | 
				
			||||||
                "INSERT IGNORE INTO channels (channel, guild_id) VALUES (?, ?)",
 | 
					                "INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
 | 
				
			||||||
                channel_id,
 | 
					                channel_id,
 | 
				
			||||||
 | 
					                channel_name,
 | 
				
			||||||
                guild_id
 | 
					                guild_id
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .execute(&pool.clone())
 | 
					            .execute(&pool.clone())
 | 
				
			||||||
@@ -50,7 +56,7 @@ impl ChannelData {
 | 
				
			|||||||
            Ok(sqlx::query_as_unchecked!(
 | 
					            Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
                Self,
 | 
					                Self,
 | 
				
			||||||
                "
 | 
					                "
 | 
				
			||||||
                SELECT id, channel, nudge, webhook_id, webhook_token, paused, paused_until
 | 
					                SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until
 | 
				
			||||||
                FROM channels
 | 
					                FROM channels
 | 
				
			||||||
                WHERE channel = ?
 | 
					                WHERE channel = ?
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
@@ -66,14 +72,18 @@ impl ChannelData {
 | 
				
			|||||||
            "
 | 
					            "
 | 
				
			||||||
            UPDATE channels
 | 
					            UPDATE channels
 | 
				
			||||||
            SET
 | 
					            SET
 | 
				
			||||||
 | 
					                name = ?,
 | 
				
			||||||
                nudge = ?,
 | 
					                nudge = ?,
 | 
				
			||||||
 | 
					                blacklisted = ?,
 | 
				
			||||||
                webhook_id = ?,
 | 
					                webhook_id = ?,
 | 
				
			||||||
                webhook_token = ?,
 | 
					                webhook_token = ?,
 | 
				
			||||||
                paused = ?,
 | 
					                paused = ?,
 | 
				
			||||||
                paused_until = ?
 | 
					                paused_until = ?
 | 
				
			||||||
            WHERE id = ?
 | 
					            WHERE id = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
 | 
					            self.name,
 | 
				
			||||||
            self.nudge,
 | 
					            self.nudge,
 | 
				
			||||||
 | 
					            self.blacklisted,
 | 
				
			||||||
            self.webhook_id,
 | 
					            self.webhook_id,
 | 
				
			||||||
            self.webhook_token,
 | 
					            self.webhook_token,
 | 
				
			||||||
            self.paused,
 | 
					            self.paused,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ pub enum RecordedCommand {
 | 
				
			|||||||
    #[serde(rename = "delete")]
 | 
					    #[serde(rename = "delete")]
 | 
				
			||||||
    Delete(crate::commands::delete::Options),
 | 
					    Delete(crate::commands::delete::Options),
 | 
				
			||||||
    #[serde(rename = "donate")]
 | 
					    #[serde(rename = "donate")]
 | 
				
			||||||
    Donate(crate::commands::donate::Options),
 | 
					    Donate(crate::commands::patreon::info::Options),
 | 
				
			||||||
    #[serde(rename = "help")]
 | 
					    #[serde(rename = "help")]
 | 
				
			||||||
    Help(crate::commands::help::Options),
 | 
					    Help(crate::commands::help::Options),
 | 
				
			||||||
    #[serde(rename = "info")]
 | 
					    #[serde(rename = "info")]
 | 
				
			||||||
@@ -111,7 +111,7 @@ impl RecordedCommand {
 | 
				
			|||||||
            "clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
 | 
					            "clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
 | 
				
			||||||
            "dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
 | 
					            "dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
 | 
				
			||||||
            "delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
 | 
					            "delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
 | 
				
			||||||
            "donate" => Some(Self::Donate(crate::commands::donate::Options::extract(ctx))),
 | 
					            "donate" => Some(Self::Donate(crate::commands::patreon::info::Options::extract(ctx))),
 | 
				
			||||||
            "help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
 | 
					            "help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
 | 
				
			||||||
            "info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
 | 
					            "info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
 | 
				
			||||||
            "look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
 | 
					            "look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ use sqlx::MySqlPool;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pub struct GuildData {
 | 
					pub struct GuildData {
 | 
				
			||||||
    pub ephemeral_confirmations: bool,
 | 
					    pub ephemeral_confirmations: bool,
 | 
				
			||||||
    pub guild_id: u64,
 | 
					    pub id: u32,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl GuildData {
 | 
					impl GuildData {
 | 
				
			||||||
@@ -13,7 +13,7 @@ impl GuildData {
 | 
				
			|||||||
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
					    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
					        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "SELECT ephemeral_confirmations, id as guild_id FROM guilds WHERE id = ?",
 | 
					            "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
 | 
				
			||||||
            guild_id.get()
 | 
					            guild_id.get()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_one(pool)
 | 
					        .fetch_one(pool)
 | 
				
			||||||
@@ -21,13 +21,13 @@ impl GuildData {
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            Ok(c)
 | 
					            Ok(c)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id.get())
 | 
					            sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.get())
 | 
				
			||||||
                .execute(&pool.clone())
 | 
					                .execute(&pool.clone())
 | 
				
			||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(sqlx::query_as_unchecked!(
 | 
					            Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
                Self,
 | 
					                Self,
 | 
				
			||||||
                "SELECT ephemeral_confirmations, id as guild_id FROM guilds WHERE id = ?",
 | 
					                "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
 | 
				
			||||||
                guild_id.get()
 | 
					                guild_id.get()
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .fetch_one(pool)
 | 
					            .fetch_one(pool)
 | 
				
			||||||
@@ -39,7 +39,7 @@ impl GuildData {
 | 
				
			|||||||
        sqlx::query!(
 | 
					        sqlx::query!(
 | 
				
			||||||
            "UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?",
 | 
					            "UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?",
 | 
				
			||||||
            self.ephemeral_confirmations,
 | 
					            self.ephemeral_confirmations,
 | 
				
			||||||
            self.guild_id
 | 
					            self.id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .execute(pool)
 | 
					        .execute(pool)
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,19 +68,16 @@ impl Data {
 | 
				
			|||||||
        guild_id: GuildId,
 | 
					        guild_id: GuildId,
 | 
				
			||||||
    ) -> Result<Vec<CommandMacro>, Error> {
 | 
					    ) -> Result<Vec<CommandMacro>, Error> {
 | 
				
			||||||
        let rows = sqlx::query!(
 | 
					        let rows = sqlx::query!(
 | 
				
			||||||
            "SELECT name, description, commands FROM command_macro WHERE guild_id = ?",
 | 
					            "SELECT name, description, commands FROM command_macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
            guild_id.get()
 | 
					            guild_id.get()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_all(&self.database)
 | 
					        .fetch_all(&self.database)
 | 
				
			||||||
        .await?
 | 
					        .await?.iter().map(|row| CommandMacro {
 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|row| CommandMacro {
 | 
					 | 
				
			||||||
            guild_id,
 | 
					            guild_id,
 | 
				
			||||||
            name: row.name.clone(),
 | 
					            name: row.name.clone(),
 | 
				
			||||||
            description: row.description.clone(),
 | 
					            description: row.description.clone(),
 | 
				
			||||||
            commands: serde_json::from_str(&row.commands.to_string()).unwrap(),
 | 
					            commands: serde_json::from_str(&row.commands.to_string()).unwrap(),
 | 
				
			||||||
        })
 | 
					        }).collect();
 | 
				
			||||||
        .collect();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(rows)
 | 
					        Ok(rows)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,8 +16,8 @@ use crate::{
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        CtxData,
 | 
					        CtxData,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    time_parser::natural_parser,
 | 
					    time_parser::{cron_next_timestamp, natural_parser},
 | 
				
			||||||
    utils::{check_guild_subscription, check_subscription},
 | 
					    utils::check_subscription,
 | 
				
			||||||
    Context, Database, Error,
 | 
					    Context, Database, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use chrono::{DateTime, NaiveDateTime, Utc};
 | 
					use chrono::{DateTime, NaiveDateTime, Utc};
 | 
				
			||||||
@@ -262,7 +262,7 @@ impl Reminder {
 | 
				
			|||||||
                            channels.id = reminders.channel_id
 | 
					                            channels.id = reminders.channel_id
 | 
				
			||||||
                        WHERE
 | 
					                        WHERE
 | 
				
			||||||
                            `status` = 'pending' AND
 | 
					                            `status` = 'pending' AND
 | 
				
			||||||
                            channels.guild_id = ?
 | 
					                            channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
				
			||||||
                        ",
 | 
					                        ",
 | 
				
			||||||
                        guild_id.get()
 | 
					                        guild_id.get()
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
@@ -486,7 +486,10 @@ pub async fn create_reminder(
 | 
				
			|||||||
    let user_data = ctx.author_data().await.unwrap();
 | 
					    let user_data = ctx.author_data().await.unwrap();
 | 
				
			||||||
    let timezone = timezone.unwrap_or(ctx.timezone().await);
 | 
					    let timezone = timezone.unwrap_or(ctx.timezone().await);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let time = natural_parser(&time, &timezone.to_string()).await;
 | 
					    let time = match cron_next_timestamp(&time, timezone) {
 | 
				
			||||||
 | 
					        Some(ts) => Some(ts),
 | 
				
			||||||
 | 
					        None => natural_parser(&time, &timezone.to_string()).await,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match time {
 | 
					    match time {
 | 
				
			||||||
        Some(time) => {
 | 
					        Some(time) => {
 | 
				
			||||||
@@ -529,9 +532,8 @@ pub async fn create_reminder(
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
 | 
					            let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
 | 
				
			||||||
                if check_subscription(&ctx, ctx.author().id).await
 | 
					                if check_subscription(&ctx, &ctx.data().database, ctx.author().id, ctx.guild_id())
 | 
				
			||||||
                    || (ctx.guild_id().is_some()
 | 
					                    .await
 | 
				
			||||||
                        && check_guild_subscription(&ctx, ctx.guild_id().unwrap()).await)
 | 
					 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    (
 | 
					                    (
 | 
				
			||||||
                        parse_duration(repeat)
 | 
					                        parse_duration(repeat)
 | 
				
			||||||
@@ -547,7 +549,7 @@ pub async fn create_reminder(
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    ctx.send(CreateReply::default().content(
 | 
					                    ctx.send(CreateReply::default().content(
 | 
				
			||||||
                        "`repeat` is only available to Patreon subscribers or self-hosted users",
 | 
					                        "`interval` is only available to Patreon subscribers or self-hosted users",
 | 
				
			||||||
                    ))
 | 
					                    ))
 | 
				
			||||||
                    .await?;
 | 
					                    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,8 +34,10 @@ use crate::{
 | 
				
			|||||||
lazy_static! {
 | 
					lazy_static! {
 | 
				
			||||||
    pub static ref TIMEFROM_REGEX: Regex =
 | 
					    pub static ref TIMEFROM_REGEX: Regex =
 | 
				
			||||||
        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
 | 
					        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
 | 
				
			||||||
    pub static ref TIMENOW_REGEX: Regex =
 | 
					    pub static ref TIMENOW_REGEX: Regex = Regex::new(
 | 
				
			||||||
        Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
 | 
					        r#"<<timenow(?:(?P<sign>[+-])(?P<offset>\d+))?:(?P<timezone>(?:\w|/|_)+?):(?P<format>.+?)?>>"#
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
    pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
 | 
					    pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -64,7 +66,7 @@ fn fmt_displacement(format: &str, seconds: u64) -> String {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn substitute(string: &str) -> String {
 | 
					pub fn substitute(string: &str) -> String {
 | 
				
			||||||
    let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
 | 
					    let new = TIMEFROM_REGEX.replace_all(string, |caps: &Captures| {
 | 
				
			||||||
        let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
 | 
					        let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
 | 
				
			||||||
        let format = caps.name("format").map(|m| m.as_str());
 | 
					        let format = caps.name("format").map(|m| m.as_str());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -92,12 +94,26 @@ pub fn substitute(string: &str) -> String {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    TIMENOW_REGEX
 | 
					    TIMENOW_REGEX
 | 
				
			||||||
        .replace(&new, |caps: &Captures| {
 | 
					        .replace_all(&new, |caps: &Captures| {
 | 
				
			||||||
            let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
 | 
					            let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
 | 
				
			||||||
            let format = caps.name("format").map(|m| m.as_str());
 | 
					            let format = caps.name("format").map(|m| m.as_str());
 | 
				
			||||||
 | 
					            let sign = caps.name("sign").map(|m| m.as_str());
 | 
				
			||||||
 | 
					            let offset = caps.name("offset").map(|m| m.as_str().parse::<i64>().ok()).flatten();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if let (Some(timezone), Some(format)) = (timezone, format) {
 | 
					            if let (Some(timezone), Some(format)) = (timezone, format) {
 | 
				
			||||||
                let now = Utc::now().with_timezone(&timezone);
 | 
					                let mut now = Utc::now().with_timezone(&timezone);
 | 
				
			||||||
 | 
					                if let (Some(sign), Some(offset)) = (sign, offset) {
 | 
				
			||||||
 | 
					                    now = now
 | 
				
			||||||
 | 
					                        .checked_add_signed(TimeDelta::seconds(
 | 
				
			||||||
 | 
					                            offset * {
 | 
				
			||||||
 | 
					                                match sign {
 | 
				
			||||||
 | 
					                                    "-" => -1,
 | 
				
			||||||
 | 
					                                    _ => 1,
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                        .unwrap_or(now)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                now.format(format).to_string()
 | 
					                now.format(format).to_string()
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ use poise::{
 | 
				
			|||||||
    CreateReply,
 | 
					    CreateReply,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde_json::Value;
 | 
					use serde_json::Value;
 | 
				
			||||||
 | 
					use serenity::all::Http;
 | 
				
			||||||
 | 
					use serenity::http::CacheHttp;
 | 
				
			||||||
use tokio::sync::Mutex;
 | 
					use tokio::sync::Mutex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{Data, Error};
 | 
					use crate::{Data, Error};
 | 
				
			||||||
@@ -19,6 +21,12 @@ pub(crate) struct TestContext<'a> {
 | 
				
			|||||||
    pub(crate) shard_id: usize,
 | 
					    pub(crate) shard_id: usize,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl CacheHttp for TestContext<'_> {
 | 
				
			||||||
 | 
					    fn http(&self) -> &Http {
 | 
				
			||||||
 | 
					        todo!()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub(crate) struct MockUser {
 | 
					pub(crate) struct MockUser {
 | 
				
			||||||
    pub(crate) id: UserId,
 | 
					    pub(crate) id: UserId,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ use std::{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
 | 
					use chrono::{DateTime, Datelike, Timelike, Utc};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use cron_parser::parse;
 | 
				
			||||||
use tokio::process::Command;
 | 
					use tokio::process::Command;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
 | 
					use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
 | 
				
			||||||
@@ -219,3 +220,7 @@ pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
        .and_then(|inner| if inner < 0 { None } else { Some(inner) })
 | 
					        .and_then(|inner| if inner < 0 { None } else { Some(inner) })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn cron_next_timestamp(expr: &str, timezone: Tz) -> Option<i64> {
 | 
				
			||||||
 | 
					    parse(expr, &Utc::now().with_timezone(&timezone)).ok().map(|next| next.timestamp() as i64)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										68
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								src/utils.rs
									
									
									
									
									
								
							@@ -9,10 +9,62 @@ use poise::{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
 | 
					    consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
 | 
				
			||||||
    ApplicationContext, Context, Error,
 | 
					    ApplicationContext, Context, Database, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
					/// Check if this user/guild combination should be considered subscribed.
 | 
				
			||||||
 | 
					/// If the guild has a patreon linked, check the user involved in the link.
 | 
				
			||||||
 | 
					/// Otherwise, check the user and the guild's owner
 | 
				
			||||||
 | 
					pub async fn check_subscription(
 | 
				
			||||||
 | 
					    ctx: impl CacheHttp,
 | 
				
			||||||
 | 
					    database: impl Executor<'_, Database = Database>,
 | 
				
			||||||
 | 
					    user_id: UserId,
 | 
				
			||||||
 | 
					    guild_id: Option<GuildId>,
 | 
				
			||||||
 | 
					) -> bool {
 | 
				
			||||||
 | 
					    if let Some(subscription_guild) = *CNC_GUILD {
 | 
				
			||||||
 | 
					        let user_subscribed = check_user_subscription(&ctx, user_id).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let owner_subscribed = match guild_id {
 | 
				
			||||||
 | 
					            Some(guild_id) => {
 | 
				
			||||||
 | 
					                if let Some(owner) = ctx.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
 | 
				
			||||||
 | 
					                    check_user_subscription(&ctx, owner).await
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    false
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => false,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let link_subscribed = match guild_id {
 | 
				
			||||||
 | 
					            Some(guild_id) => {
 | 
				
			||||||
 | 
					                if let Ok(row) = sqlx::query!(
 | 
				
			||||||
 | 
					                    "SELECT user_id FROM patreon_link WHERE guild_id = ?",
 | 
				
			||||||
 | 
					                    guild_id.get()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_one(database)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    check_user_subscription(&ctx, row.user_id).await
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    false
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => false,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user_subscribed || owner_subscribed || link_subscribed
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Check a user's subscription status, ignoring Patreon linkage
 | 
				
			||||||
 | 
					pub async fn check_user_subscription(
 | 
				
			||||||
 | 
					    cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					    user_id: impl Into<UserId>,
 | 
				
			||||||
 | 
					) -> bool {
 | 
				
			||||||
    if let Some(subscription_guild) = *CNC_GUILD {
 | 
					    if let Some(subscription_guild) = *CNC_GUILD {
 | 
				
			||||||
        let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
 | 
					        let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,17 +82,6 @@ pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<U
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn check_guild_subscription(
 | 
					 | 
				
			||||||
    cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
    guild_id: impl Into<GuildId>,
 | 
					 | 
				
			||||||
) -> bool {
 | 
					 | 
				
			||||||
    if let Some(owner) = cache_http.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
 | 
					 | 
				
			||||||
        check_subscription(&cache_http, owner).await
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn reply_to_interaction_response_message(
 | 
					pub fn reply_to_interaction_response_message(
 | 
				
			||||||
    reply: CreateReply,
 | 
					    reply: CreateReply,
 | 
				
			||||||
) -> CreateInteractionResponseMessage {
 | 
					) -> CreateInteractionResponseMessage {
 | 
				
			||||||
@@ -76,6 +117,7 @@ pub trait Extract {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use extract_derive::Extract;
 | 
					pub use extract_derive::Extract;
 | 
				
			||||||
 | 
					use sqlx::Executor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
macro_rules! extract_arg {
 | 
					macro_rules! extract_arg {
 | 
				
			||||||
    ($ctx:ident, $name:ident, String) => {
 | 
					    ($ctx:ident, $name:ident, String) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,6 +92,8 @@ enum Error {
 | 
				
			|||||||
    SQLx(sqlx::Error),
 | 
					    SQLx(sqlx::Error),
 | 
				
			||||||
    #[allow(unused)]
 | 
					    #[allow(unused)]
 | 
				
			||||||
    Serenity(serenity::Error),
 | 
					    Serenity(serenity::Error),
 | 
				
			||||||
 | 
					    #[allow(unused)]
 | 
				
			||||||
 | 
					    MissingDiscordPermission(&'static str),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn initialize(
 | 
					pub async fn initialize(
 | 
				
			||||||
@@ -231,39 +233,6 @@ pub async fn initialize(
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
					 | 
				
			||||||
    offline!(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Some(subscription_guild) = *CNC_GUILD {
 | 
					 | 
				
			||||||
        let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Ok(member) = guild_member {
 | 
					 | 
				
			||||||
            for role in member.roles {
 | 
					 | 
				
			||||||
                if SUBSCRIPTION_ROLES.contains(&role.get()) {
 | 
					 | 
				
			||||||
                    return true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn check_guild_subscription(
 | 
					 | 
				
			||||||
    cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
    guild_id: impl Into<GuildId>,
 | 
					 | 
				
			||||||
) -> bool {
 | 
					 | 
				
			||||||
    offline!(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Some(owner) = cache_http.cache().unwrap().guild(guild_id).map(|guild| guild.owner_id) {
 | 
					 | 
				
			||||||
        check_subscription(&cache_http, owner).await
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn check_authorization(
 | 
					pub async fn check_authorization(
 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &Context,
 | 
					    ctx: &Context,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,38 +7,43 @@ pub mod todos;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use std::env;
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utils::check_subscription;
 | 
				
			||||||
 | 
					use crate::web::guards::transaction::Transaction;
 | 
				
			||||||
 | 
					use crate::web::{check_authorization, routes::JsonResult};
 | 
				
			||||||
pub use channels::get_guild_channels;
 | 
					pub use channels::get_guild_channels;
 | 
				
			||||||
pub use emojis::get_guild_emojis;
 | 
					pub use emojis::get_guild_emojis;
 | 
				
			||||||
pub use reminders::*;
 | 
					pub use reminders::*;
 | 
				
			||||||
use rocket::{get, http::CookieJar, serde::json::json, State};
 | 
					use rocket::{get, http::CookieJar, serde::json::json, State};
 | 
				
			||||||
pub use roles::get_guild_roles;
 | 
					pub use roles::get_guild_roles;
 | 
				
			||||||
 | 
					use serenity::all::UserId;
 | 
				
			||||||
use serenity::{
 | 
					use serenity::{
 | 
				
			||||||
    client::Context,
 | 
					    client::Context,
 | 
				
			||||||
    model::id::{GuildId, RoleId},
 | 
					    model::id::{GuildId, RoleId},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
pub use templates::*;
 | 
					pub use templates::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::web::{check_authorization, routes::JsonResult};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>")]
 | 
					#[get("/api/guild/<id>")]
 | 
				
			||||||
pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
 | 
					pub async fn get_guild_info(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    mut transaction: Transaction<'_>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
    offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
 | 
					    offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
 | 
				
			||||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match GuildId::new(id)
 | 
					    let user_id =
 | 
				
			||||||
        .to_guild_cached(ctx.inner())
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
        .map(|guild| (guild.owner_id, guild.name.clone()))
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Some((owner_id, name)) => {
 | 
					 | 
				
			||||||
            let member_res = GuildId::new(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
 | 
					 | 
				
			||||||
                .member(&ctx.inner(), owner_id)
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let patreon = member_res.map_or(false, |member| {
 | 
					    match GuildId::new(id).to_guild_cached(ctx.inner()).map(|guild| guild.name.clone()) {
 | 
				
			||||||
                member
 | 
					        Some(name) => {
 | 
				
			||||||
                    .roles
 | 
					            let patreon = check_subscription(
 | 
				
			||||||
                    .contains(&RoleId::new(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
					                ctx.inner(),
 | 
				
			||||||
            });
 | 
					                transaction.executor(),
 | 
				
			||||||
 | 
					                UserId::from(user_id),
 | 
				
			||||||
 | 
					                Some(GuildId::from(id)),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(json!({ "patreon": patreon, "name": name }))
 | 
					            Ok(json!({ "patreon": patreon, "name": name }))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,8 +12,9 @@ use serenity::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utils::check_subscription;
 | 
				
			||||||
use crate::web::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_authorization, check_guild_subscription, check_subscription,
 | 
					    check_authorization,
 | 
				
			||||||
    consts::MIN_INTERVAL,
 | 
					    consts::MIN_INTERVAL,
 | 
				
			||||||
    guards::transaction::Transaction,
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
    routes::{
 | 
					    routes::{
 | 
				
			||||||
@@ -186,8 +187,13 @@ pub async fn edit_reminder(
 | 
				
			|||||||
        || reminder.interval_months.flatten().is_some()
 | 
					        || reminder.interval_months.flatten().is_some()
 | 
				
			||||||
        || reminder.interval_seconds.flatten().is_some()
 | 
					        || reminder.interval_seconds.flatten().is_some()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if check_guild_subscription(&ctx.inner(), id).await
 | 
					        if check_subscription(
 | 
				
			||||||
            || check_subscription(&ctx.inner(), user_id).await
 | 
					            ctx.inner(),
 | 
				
			||||||
 | 
					            transaction.executor(),
 | 
				
			||||||
 | 
					            UserId::from(user_id),
 | 
				
			||||||
 | 
					            Some(GuildId::from(id)),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            let new_interval_length = match reminder.interval_days {
 | 
					            let new_interval_length = match reminder.interval_days {
 | 
				
			||||||
                Some(interval) => interval.unwrap_or(0),
 | 
					                Some(interval) => interval.unwrap_or(0),
 | 
				
			||||||
@@ -305,11 +311,19 @@ pub async fn edit_reminder(
 | 
				
			|||||||
                        Err(e) => {
 | 
					                        Err(e) => {
 | 
				
			||||||
                            warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
					                            warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            // Provide more specific error messages based on the error type
 | 
				
			||||||
 | 
					                            match e {
 | 
				
			||||||
 | 
					                                crate::web::Error::MissingDiscordPermission(permission) => {
 | 
				
			||||||
 | 
					                                    error.push(format!("Please ensure the bot has the \"{}\" permission in the channel", permission));
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                _ => {
 | 
				
			||||||
                                    error.push("Failed to configure channel for reminders. Please check the bot permissions".to_string());
 | 
					                                    error.push("Failed to configure channel for reminders. Please check the bot permissions".to_string());
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            None => {
 | 
					            None => {
 | 
				
			||||||
                warn!(
 | 
					                warn!(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,7 +31,7 @@ pub async fn get_reminder_templates(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
        ReminderTemplate,
 | 
					        ReminderTemplate,
 | 
				
			||||||
        "SELECT * FROM reminder_template WHERE guild_id = ?",
 | 
					        "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
        id
 | 
					        id
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
@@ -87,7 +87,7 @@ pub async fn delete_reminder_template(
 | 
				
			|||||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match sqlx::query!(
 | 
					    match sqlx::query!(
 | 
				
			||||||
        "DELETE FROM reminder_template WHERE guild_id = ? AND id = ?",
 | 
					        "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
 | 
				
			||||||
        id, delete_reminder_template.id
 | 
					        id, delete_reminder_template.id
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -66,7 +66,7 @@ pub async fn create_todo(
 | 
				
			|||||||
                "
 | 
					                "
 | 
				
			||||||
                INSERT INTO todos (guild_id, channel_id, value)
 | 
					                INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
                VALUES (
 | 
					                VALUES (
 | 
				
			||||||
                    ?,
 | 
					                    (SELECT id FROM guilds WHERE guild = ?),
 | 
				
			||||||
                    (SELECT id FROM channels WHERE channel = ?),
 | 
					                    (SELECT id FROM channels WHERE channel = ?),
 | 
				
			||||||
                    ?
 | 
					                    ?
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@@ -88,7 +88,7 @@ pub async fn create_todo(
 | 
				
			|||||||
                "
 | 
					                "
 | 
				
			||||||
                INSERT INTO todos (guild_id, channel_id, value)
 | 
					                INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
                VALUES (
 | 
					                VALUES (
 | 
				
			||||||
                    ?,
 | 
					                    (SELECT id FROM guilds WHERE guild = ?),
 | 
				
			||||||
                    NULL,
 | 
					                    NULL,
 | 
				
			||||||
                    ?
 | 
					                    ?
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@@ -130,9 +130,11 @@ pub async fn get_todo(
 | 
				
			|||||||
            channels.channel AS channel_id,
 | 
					            channels.channel AS channel_id,
 | 
				
			||||||
            value
 | 
					            value
 | 
				
			||||||
        FROM todos
 | 
					        FROM todos
 | 
				
			||||||
 | 
					        INNER JOIN guilds
 | 
				
			||||||
 | 
					        ON guilds.id = todos.guild_id
 | 
				
			||||||
        LEFT JOIN channels
 | 
					        LEFT JOIN channels
 | 
				
			||||||
        ON channels.id = todos.channel_id
 | 
					        ON channels.id = todos.channel_id
 | 
				
			||||||
        WHERE todos.guild_id = ?
 | 
					        WHERE guilds.guild = ?
 | 
				
			||||||
        ",
 | 
					        ",
 | 
				
			||||||
        id
 | 
					        id
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@@ -165,7 +167,7 @@ pub async fn update_todo(
 | 
				
			|||||||
        "
 | 
					        "
 | 
				
			||||||
        UPDATE todos
 | 
					        UPDATE todos
 | 
				
			||||||
        SET value = ?
 | 
					        SET value = ?
 | 
				
			||||||
        WHERE guild_id = ?
 | 
					        WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
				
			||||||
            AND id = ?
 | 
					            AND id = ?
 | 
				
			||||||
        ",
 | 
					        ",
 | 
				
			||||||
        todo.value,
 | 
					        todo.value,
 | 
				
			||||||
@@ -200,7 +202,7 @@ pub async fn delete_todo(
 | 
				
			|||||||
    sqlx::query!(
 | 
					    sqlx::query!(
 | 
				
			||||||
        "
 | 
					        "
 | 
				
			||||||
        DELETE FROM todos
 | 
					        DELETE FROM todos
 | 
				
			||||||
        WHERE guild_id = ?
 | 
					        WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
				
			||||||
            AND id = ?
 | 
					            AND id = ?
 | 
				
			||||||
        ",
 | 
					        ",
 | 
				
			||||||
        guild_id,
 | 
					        guild_id,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
use serenity::{client::Context, model::id::UserId};
 | 
					use serenity::{client::Context, model::id::UserId};
 | 
				
			||||||
use sqlx::types::Json;
 | 
					use sqlx::types::Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utils::check_subscription;
 | 
				
			||||||
use crate::web::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_subscription,
 | 
					 | 
				
			||||||
    consts::{
 | 
					    consts::{
 | 
				
			||||||
        DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
					        DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
				
			||||||
        MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
 | 
					        MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
 | 
				
			||||||
@@ -65,7 +65,16 @@ pub async fn create_reminder(
 | 
				
			|||||||
    if let Err(e) = channel {
 | 
					    if let Err(e) = channel {
 | 
				
			||||||
        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
					        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Err(json!({"error": "Failed to configure channel for reminders."}));
 | 
					        // Provide more specific error messages based on the error type
 | 
				
			||||||
 | 
					        let error_msg = match e {
 | 
				
			||||||
 | 
					            Error::MissingDiscordPermission(permission) => format!(
 | 
				
			||||||
 | 
					                "Please ensure the bot has the \"{}\" permission in the channel",
 | 
				
			||||||
 | 
					                permission
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            _ => "Failed to configure channel for reminders.".to_string(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Err(json!({"error": error_msg}));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let channel = channel.unwrap();
 | 
					    let channel = channel.unwrap();
 | 
				
			||||||
@@ -122,7 +131,7 @@ pub async fn create_reminder(
 | 
				
			|||||||
        || reminder.interval_days.is_some()
 | 
					        || reminder.interval_days.is_some()
 | 
				
			||||||
        || reminder.interval_months.is_some()
 | 
					        || reminder.interval_months.is_some()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if !check_subscription(&ctx, user_id).await {
 | 
					        if !check_subscription(&ctx, transaction.executor(), user_id, None).await {
 | 
				
			||||||
            return Err(json!({"error": "Patreon is required to set intervals"}));
 | 
					            return Err(json!({"error": "Patreon is required to set intervals"}));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,8 +9,8 @@ use rocket::{
 | 
				
			|||||||
use serenity::{client::Context, model::id::UserId};
 | 
					use serenity::{client::Context, model::id::UserId};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utils::check_subscription;
 | 
				
			||||||
use crate::web::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_subscription,
 | 
					 | 
				
			||||||
    guards::transaction::Transaction,
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
    routes::{
 | 
					    routes::{
 | 
				
			||||||
        dashboard::{
 | 
					        dashboard::{
 | 
				
			||||||
@@ -162,7 +162,9 @@ pub async fn edit_reminder(
 | 
				
			|||||||
        || reminder.interval_months.flatten().is_some()
 | 
					        || reminder.interval_months.flatten().is_some()
 | 
				
			||||||
        || reminder.interval_seconds.flatten().is_some()
 | 
					        || reminder.interval_seconds.flatten().is_some()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if check_subscription(&ctx.inner(), user_id).await {
 | 
					        if check_subscription(&ctx.inner(), transaction.executor(), UserId::from(user_id), None)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            let new_interval_length = match reminder.interval_days {
 | 
					            let new_interval_length = match reminder.interval_days {
 | 
				
			||||||
                Some(interval) => interval.unwrap_or(0),
 | 
					                Some(interval) => interval.unwrap_or(0),
 | 
				
			||||||
                None => sqlx::query!(
 | 
					                None => sqlx::query!(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,8 +39,7 @@ pub async fn export(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
        ReminderTemplateCsv,
 | 
					        ReminderTemplateCsv,
 | 
				
			||||||
        "
 | 
					        "SELECT
 | 
				
			||||||
        SELECT
 | 
					 | 
				
			||||||
         name,
 | 
					         name,
 | 
				
			||||||
         attachment,
 | 
					         attachment,
 | 
				
			||||||
         attachment_name,
 | 
					         attachment_name,
 | 
				
			||||||
@@ -61,9 +60,7 @@ pub async fn export(
 | 
				
			|||||||
         interval_months,
 | 
					         interval_months,
 | 
				
			||||||
         tts,
 | 
					         tts,
 | 
				
			||||||
         username
 | 
					         username
 | 
				
			||||||
        FROM reminder_template
 | 
					        FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
        WHERE guild_id = ?
 | 
					 | 
				
			||||||
        ",
 | 
					 | 
				
			||||||
        id
 | 
					        id
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,11 +38,10 @@ pub async fn export(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
        TodoCsv,
 | 
					        TodoCsv,
 | 
				
			||||||
        "
 | 
					        "SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
 | 
				
			||||||
        SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
 | 
					 | 
				
			||||||
        LEFT JOIN channels ON todos.channel_id = channels.id
 | 
					        LEFT JOIN channels ON todos.channel_id = channels.id
 | 
				
			||||||
        INNER JOIN guilds ON todos.guild_id = ?
 | 
					        INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			||||||
        ",
 | 
					        WHERE guilds.guild = ?",
 | 
				
			||||||
        id
 | 
					        id
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
@@ -97,7 +96,7 @@ pub async fn import(
 | 
				
			|||||||
            Ok(body) => {
 | 
					            Ok(body) => {
 | 
				
			||||||
                let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
					                let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), ?)";
 | 
					                let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?))";
 | 
				
			||||||
                let mut query_params = vec![];
 | 
					                let mut query_params = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                for result in reader.deserialize::<TodoCsv>() {
 | 
					                for result in reader.deserialize::<TodoCsv>() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ use rocket::{
 | 
				
			|||||||
use rocket_dyn_templates::Template;
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
use secrecy::ExposeSecret;
 | 
					use secrecy::ExposeSecret;
 | 
				
			||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
 | 
					use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
 | 
				
			||||||
 | 
					use serenity::http::HttpError;
 | 
				
			||||||
use serenity::{
 | 
					use serenity::{
 | 
				
			||||||
    all::CacheHttp,
 | 
					    all::CacheHttp,
 | 
				
			||||||
    builder::CreateWebhook,
 | 
					    builder::CreateWebhook,
 | 
				
			||||||
@@ -19,9 +20,9 @@ use serenity::{
 | 
				
			|||||||
use sqlx::types::Json;
 | 
					use sqlx::types::Json;
 | 
				
			||||||
use sqlx::FromRow;
 | 
					use sqlx::FromRow;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utils::check_subscription;
 | 
				
			||||||
use crate::web::{
 | 
					use crate::web::{
 | 
				
			||||||
    catchers::internal_server_error,
 | 
					    catchers::internal_server_error,
 | 
				
			||||||
    check_guild_subscription, check_subscription,
 | 
					 | 
				
			||||||
    consts::{
 | 
					    consts::{
 | 
				
			||||||
        CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH,
 | 
					        CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH,
 | 
				
			||||||
        MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH,
 | 
					        MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH,
 | 
				
			||||||
@@ -373,12 +374,12 @@ pub(crate) async fn create_reminder(
 | 
				
			|||||||
    reminder: CreateReminder,
 | 
					    reminder: CreateReminder,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonResult {
 | 
				
			||||||
    // check guild in db
 | 
					    // check guild in db
 | 
				
			||||||
    match sqlx::query!("SELECT 1 as A FROM guilds WHERE id = ?", guild_id.get())
 | 
					    match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.get())
 | 
				
			||||||
        .fetch_one(transaction.executor())
 | 
					        .fetch_one(transaction.executor())
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Err(sqlx::Error::RowNotFound) => {
 | 
					        Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
            if sqlx::query!("INSERT INTO guilds (id) VALUES (?)", guild_id.get())
 | 
					            if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.get())
 | 
				
			||||||
                .execute(transaction.executor())
 | 
					                .execute(transaction.executor())
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .is_err()
 | 
					                .is_err()
 | 
				
			||||||
@@ -404,9 +405,19 @@ pub(crate) async fn create_reminder(
 | 
				
			|||||||
    if let Err(e) = channel {
 | 
					    if let Err(e) = channel {
 | 
				
			||||||
        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
					        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Err(
 | 
					        // Provide more specific error messages based on the error type
 | 
				
			||||||
            json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
 | 
					        let error_msg = match e {
 | 
				
			||||||
        );
 | 
					            Error::MissingDiscordPermission(permission) => {
 | 
				
			||||||
 | 
					                format!(
 | 
				
			||||||
 | 
					                    "Please ensure the bot has the \"{}\" permission in the channel",
 | 
				
			||||||
 | 
					                    permission
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => "Failed to configure channel for reminders. Please check the bot permissions"
 | 
				
			||||||
 | 
					                .to_string(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Err(json!({"error": error_msg}));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let channel = channel.unwrap();
 | 
					    let channel = channel.unwrap();
 | 
				
			||||||
@@ -466,9 +477,7 @@ pub(crate) async fn create_reminder(
 | 
				
			|||||||
        || reminder.interval_days.is_some()
 | 
					        || reminder.interval_days.is_some()
 | 
				
			||||||
        || reminder.interval_months.is_some()
 | 
					        || reminder.interval_months.is_some()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if !check_guild_subscription(&ctx, guild_id).await
 | 
					        if !check_subscription(&ctx, transaction.executor(), user_id, Some(guild_id)).await {
 | 
				
			||||||
            && !check_subscription(&ctx, user_id).await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return Err(json!({"error": "Patreon is required to set intervals"}));
 | 
					            return Err(json!({"error": "Patreon is required to set intervals"}));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -659,7 +668,7 @@ pub(crate) async fn create_reminder_template(
 | 
				
			|||||||
         interval_months,
 | 
					         interval_months,
 | 
				
			||||||
         tts,
 | 
					         tts,
 | 
				
			||||||
         username
 | 
					         username
 | 
				
			||||||
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
 | 
					        ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
 | 
				
			||||||
         ?, ?, ?, ?, ?, ?, ?)",
 | 
					         ?, ?, ?, ?, ?, ?, ?)",
 | 
				
			||||||
        guild_id.get(),
 | 
					        guild_id.get(),
 | 
				
			||||||
        name,
 | 
					        name,
 | 
				
			||||||
@@ -716,13 +725,36 @@ async fn create_database_channel(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    match row {
 | 
					    match row {
 | 
				
			||||||
        Ok(row) => {
 | 
					        Ok(row) => {
 | 
				
			||||||
            let is_dm =
 | 
					            let is_dm = channel
 | 
				
			||||||
                channel.to_channel(&ctx).await.map_err(|e| Error::Serenity(e))?.private().is_some();
 | 
					                .to_channel(&ctx)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .map_err(|e| {
 | 
				
			||||||
 | 
					                    if let serenity::Error::Http(http_error) = &e {
 | 
				
			||||||
 | 
					                        if let HttpError::UnsuccessfulRequest(response) = http_error {
 | 
				
			||||||
 | 
					                            if response.error.code == 50001 {
 | 
				
			||||||
 | 
					                                return Error::MissingDiscordPermission("View Channel");
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Error::Serenity(e)
 | 
				
			||||||
 | 
					                })?
 | 
				
			||||||
 | 
					                .private()
 | 
				
			||||||
 | 
					                .is_some();
 | 
				
			||||||
            if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
 | 
					            if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
 | 
				
			||||||
                let webhook = channel
 | 
					                let webhook = channel
 | 
				
			||||||
                    .create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
 | 
					                    .create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
 | 
				
			||||||
                    .await
 | 
					                    .await
 | 
				
			||||||
                    .map_err(|e| Error::Serenity(e))?;
 | 
					                    .map_err(|e| match &e {
 | 
				
			||||||
 | 
					                        serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
 | 
				
			||||||
 | 
					                            match response.error.code {
 | 
				
			||||||
 | 
					                                50001 => Error::MissingDiscordPermission("View Channel"),
 | 
				
			||||||
 | 
					                                50013 => Error::MissingDiscordPermission("Manage Webhooks"),
 | 
				
			||||||
 | 
					                                _ => Error::Serenity(e),
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        _ => Error::Serenity(e),
 | 
				
			||||||
 | 
					                    })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let token = webhook.token.unwrap();
 | 
					                let token = webhook.token.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -747,7 +779,16 @@ async fn create_database_channel(
 | 
				
			|||||||
            let webhook = channel
 | 
					            let webhook = channel
 | 
				
			||||||
                .create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
 | 
					                .create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|e| Error::Serenity(e))?;
 | 
					                .map_err(|e| match &e {
 | 
				
			||||||
 | 
					                    serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
 | 
				
			||||||
 | 
					                        match response.error.code {
 | 
				
			||||||
 | 
					                            50001 => Error::MissingDiscordPermission("View Channel"),
 | 
				
			||||||
 | 
					                            50013 => Error::MissingDiscordPermission("Manage Webhooks"),
 | 
				
			||||||
 | 
					                            _ => Error::Serenity(e),
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    _ => Error::Serenity(e),
 | 
				
			||||||
 | 
					                })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let token = webhook.token.unwrap();
 | 
					            let token = webhook.token.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -806,22 +847,15 @@ pub async fn todos_redirect(id: &str) -> Redirect {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#[get("/")]
 | 
					#[get("/")]
 | 
				
			||||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
 | 
					pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
 | 
				
			||||||
    if cookies.get_private("userid").is_some() {
 | 
					    render_dashboard(cookies).await
 | 
				
			||||||
        match NamedFile::open(Path::new(path!("static/index.html"))).await {
 | 
					 | 
				
			||||||
            Ok(f) => DashboardPage::Ok(f),
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Couldn't render dashboard: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                DashboardPage::NotConfigured(internal_server_error().await)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        DashboardPage::Unauthorised(Redirect::to("/login/discord"))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[get("/<_..>")]
 | 
					#[get("/<_..>")]
 | 
				
			||||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
 | 
					pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
 | 
				
			||||||
 | 
					    render_dashboard(cookies).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn render_dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
 | 
				
			||||||
    if cookies.get_private("userid").is_some() {
 | 
					    if cookies.get_private("userid").is_some() {
 | 
				
			||||||
        match NamedFile::open(Path::new(path!("static/index.html"))).await {
 | 
					        match NamedFile::open(Path::new(path!("static/index.html"))).await {
 | 
				
			||||||
            Ok(f) => DashboardPage::Ok(f),
 | 
					            Ok(f) => DashboardPage::Ok(f),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,24 @@
 | 
				
			|||||||
                    Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
 | 
					                    Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
 | 
				
			||||||
                    for the reminder.
 | 
					                    for the reminder.
 | 
				
			||||||
                </p>
 | 
					                </p>
 | 
				
			||||||
 | 
					                <p class="subtitle">Time</p>
 | 
				
			||||||
 | 
					                <p class="content">
 | 
				
			||||||
 | 
					                    The bot will take a "best-guess" at what time you entered. It will favour UK date formats
 | 
				
			||||||
 | 
					                    over US date formats (MM/DD/YY) where possible.
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    You can also use <code>cron</code>-like syntax to specify the time. For example, using
 | 
				
			||||||
 | 
					                    <code>0 0 1 * *</code> will send the reminder at midnight on the first of the next month.
 | 
				
			||||||
 | 
					                    For more information on cron syntax, see <a href="https://crontab.guru/">crontab.guru</a>.
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    <strong>Cron syntax is not repeating</strong>. Please use the optional "interval" field to specify a repetition interval.
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <p class="subtitle">Pings</p>
 | 
				
			||||||
 | 
					                <p class="content">
 | 
				
			||||||
 | 
					                    Roles and users can be pinged by including their @ mention in the "content" field.
 | 
				
			||||||
 | 
					                    To ping a role, the role must be set as mentionable, and the bot must have permissions to mention the role.
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    Please note that when using the dashboard, roles can only be pinged in the "Content..." field and not the embed fields.
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
@@ -37,4 +55,40 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <section class="hero is-small">
 | 
				
			||||||
 | 
					        <div class="hero-body">
 | 
				
			||||||
 | 
					            <div class="container">
 | 
				
			||||||
 | 
					                <p class="title">Custom formatting rules</p>
 | 
				
			||||||
 | 
					                <p class="content">
 | 
				
			||||||
 | 
					                    Reminder content can be customized using formatting rules.
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <p class="subtitle">timefrom</p>
 | 
				
			||||||
 | 
					                <p class="content">
 | 
				
			||||||
 | 
					                    The <code>timefrom</code> formatting rule will display a formatted difference
 | 
				
			||||||
 | 
					                    between the time the reminder sends and a specified time.
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    For example, if the current time is 1755800000 (UNIX time), the format string
 | 
				
			||||||
 | 
					                    <code><<timefrom:1755803600>></code> would display "1 hour"
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <p class="subtitle">timenow</p>
 | 
				
			||||||
 | 
					                <p class="content">
 | 
				
			||||||
 | 
					                    The <code>timenow</code> formatting rule displays the current time or an offset
 | 
				
			||||||
 | 
					                    from the current time in a given timezone in a custom format.
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    For example, if the current time is 1755800000 (UNIX time), the format string
 | 
				
			||||||
 | 
					                    <code><<timenow:UTC:%H:%M:%S>></code> would display "18:13:20"
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    Optionally, an offset can be provided to display a time from your current time.
 | 
				
			||||||
 | 
					                    For example, if the current time is 1755800000 (UNIX time), the format string
 | 
				
			||||||
 | 
					                    <code><<timenow+120:UTC:%H:%M:%S>></code> would display "18:15:20",
 | 
				
			||||||
 | 
					                    or <code><<timenow-120:UTC:%H:%M:%S>></code> would display "18:11:20"
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    You can use this feature alongside Discord's timestamp formatting. The following
 | 
				
			||||||
 | 
					                    will show the text "in 2 minutes" for all users as a Discord timestamp:
 | 
				
			||||||
 | 
					                    <code><t:<<timenow+120:UTC:%s>>:R></code>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user