├── .gitignore ├── README.md ├── discord ├── structs.v └── timestamp.v ├── viscord.v └── viscord ├── connection.v ├── packets.v └── rest.v /.gitignore: -------------------------------------------------------------------------------- 1 | vis -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # viscord 2 | -------------------------------------------------------------------------------- /discord/structs.v: -------------------------------------------------------------------------------- 1 | module discord 2 | 3 | // ======================================= // 4 | // Hey, fyi, a lot of this has to be // 5 | // commented out to compile due to // 6 | // optionals being limited to 400b // 7 | // this will be fixed in the future // 8 | // ======================================= // 9 | // In the future i plan to move all // 10 | // of these structs into their own // 11 | // files based on their relationship // 12 | // ======================================= // 13 | 14 | pub enum ActivityTypes { 15 | game 16 | streaming 17 | listening 18 | custom 19 | } 20 | pub struct ActivityTimestamps { 21 | pub: 22 | start int 23 | end int 24 | } 25 | pub struct ActivityEmoji { 26 | pub: 27 | name string 28 | id string 29 | animated bool 30 | } 31 | pub struct ActivityParty { 32 | pub: 33 | id string 34 | size []int 35 | } 36 | pub struct ActivityAssets { 37 | pub: 38 | large_image string 39 | large_text string 40 | small_image string 41 | small_text string 42 | } 43 | pub struct ActivitySecrets { 44 | pub: 45 | join string 46 | spectate string 47 | match_str string [json:"match"] 48 | } 49 | pub struct Activity { 50 | pub: 51 | name string 52 | typ ActivityTypes [json:"type"] 53 | url string 54 | created_at int 55 | timestamps ActivityTimestamps 56 | application_id string 57 | details string 58 | state string 59 | emoji ActivityEmoji 60 | party ActivityParty 61 | assets ActivityAssets 62 | secrets ActivitySecrets 63 | flags int 64 | } 65 | 66 | pub struct Status { 67 | pub: 68 | since int 69 | game Activity 70 | status string 71 | afk bool 72 | } 73 | 74 | pub struct User { 75 | pub: 76 | id string 77 | username string 78 | discriminator string 79 | avatar string = "" 80 | bot bool = false 81 | system bool = false 82 | mfa_enabled bool = false 83 | locale string = "" 84 | verified bool = false 85 | email string = "" 86 | flags int = 0 87 | premium_type int = 0 88 | public_flags int = 0 89 | } 90 | 91 | pub struct GuildMember { 92 | pub: 93 | nick string 94 | roles []string 95 | joined_at string 96 | premium_since string 97 | deaf bool 98 | mute bool 99 | } 100 | 101 | pub struct ChannelMention { 102 | pub: 103 | id string 104 | guild_id string 105 | typ int [json:"type"] 106 | name string 107 | } 108 | 109 | pub struct Attachment { 110 | pub: 111 | id string 112 | filename string 113 | size int 114 | url string 115 | proxy_url string 116 | height int 117 | width int 118 | } 119 | 120 | pub struct EmbedFooter { 121 | pub: 122 | text string 123 | icon_url string 124 | proxy_icon_url string 125 | } 126 | pub struct EmbedImage { 127 | pub: 128 | url string 129 | proxy_url string 130 | height int 131 | width int 132 | } 133 | pub struct EmbedProvider { 134 | pub: 135 | name string 136 | url string 137 | } 138 | pub struct EmbedAuthor { 139 | pub: 140 | name string 141 | url string 142 | icon_url string 143 | proxy_icon_url string 144 | } 145 | pub struct EmbedField { 146 | pub: 147 | name string 148 | value string 149 | inline bool 150 | } 151 | pub struct Embed { 152 | pub: 153 | title string 154 | typ string [json:"type"] 155 | description string 156 | url string 157 | timestamp string 158 | color int 159 | footer EmbedFooter 160 | image EmbedImage 161 | thumbnail EmbedImage 162 | video EmbedImage 163 | provider EmbedProvider 164 | author EmbedAuthor 165 | fields []EmbedField 166 | } 167 | 168 | pub struct Emoji { 169 | pub: 170 | id string 171 | name string 172 | roles []string 173 | //user User 174 | require_colons bool 175 | managed bool 176 | animated bool 177 | available bool 178 | } 179 | pub struct Reaction { 180 | pub: 181 | count int 182 | me bool 183 | emoji Emoji 184 | } 185 | 186 | pub enum MessageTypes { 187 | default_message 188 | recipient_add 189 | recipient_remove 190 | call 191 | channel_name_change 192 | channel_icon_change 193 | channel_pinned_message 194 | guild_member_join 195 | user_premium_guild_subscription 196 | user_premium_guild_subscription_tier_1 197 | user_premium_guild_subscription_tier_2 198 | user_premium_guild_subscription_tier_3 199 | channel_follow_add 200 | guild_discovery_disqualified 201 | guild_discovery_requalifie 202 | } 203 | pub enum MessageFlags { 204 | crossposted 205 | is_crospost 206 | suppress_embeds 207 | source_message_deleted 208 | urgent 209 | } 210 | pub enum MessageActivityType { 211 | join 212 | spectate 213 | listen 214 | join_request 215 | } 216 | pub struct MessageActivity { 217 | pub: 218 | typ MessageActivityType [json:"type"] 219 | party_id string 220 | } 221 | pub struct MessageApplication { 222 | pub: 223 | id string 224 | cover_image string 225 | description string 226 | icon string 227 | name string 228 | } 229 | pub struct MessageReference { 230 | pub: 231 | message_id string 232 | channel_id string 233 | guild_id string 234 | } 235 | pub struct Message { 236 | pub: 237 | id string 238 | channel_id string 239 | guild_id string 240 | author User 241 | member GuildMember 242 | content string 243 | timestamp string 244 | edited_timestamp string 245 | tts bool 246 | mention_everyone bool 247 | mentions []User 248 | //mention_roles []string 249 | //mention_channels []ChannelMention 250 | //attachments []Attachment 251 | //embeds []Embed 252 | //reactions []Reaction 253 | nonce string 254 | pinned bool 255 | webhook_id string 256 | typ int [json:"type"] 257 | //activity MessageActivity 258 | //application MessageApplication 259 | //message_reference MessageReference 260 | flags MessageFlags 261 | } 262 | 263 | pub struct UnavailableGuild { 264 | pub: 265 | id string 266 | unavailable bool 267 | } -------------------------------------------------------------------------------- /discord/timestamp.v: -------------------------------------------------------------------------------- 1 | module discord 2 | import time 3 | 4 | pub fn parse_timestamp(s string) time.Time { 5 | mut start := s.index('T') or { 0 } 6 | start++ 7 | end := s.index('+') or { 1 } 8 | symd := s[0..start] 9 | ymd := symd.split('-') 10 | year := ymd[0] 11 | month := ymd[1] 12 | day := ymd[2] 13 | shms := s[start..end] 14 | hms := shms.split(':') 15 | hour := hms[0] 16 | minute := hms[1] 17 | second := hms[2] 18 | 19 | res := time.new_time(time.Time{ 20 | year: ymd[0].int() 21 | month: ymd[1].int() 22 | day: ymd[2].int() 23 | hour: hour.int() 24 | minute: minute.int() 25 | second: second.int() 26 | }) 27 | return res 28 | } -------------------------------------------------------------------------------- /viscord.v: -------------------------------------------------------------------------------- 1 | module main 2 | import discord 3 | import viscord 4 | import json 5 | 6 | struct App { 7 | timestamps map[string][]int 8 | mutestamps map[string][]int 9 | mutes map[string]bool 10 | owners []string 11 | c &viscord.Connection 12 | } 13 | 14 | fn main () { 15 | mut c := viscord.new_connection(viscord.ConnectionConfig{ 16 | gateway: 'wss://gateway.discord.gg:443/?encoding=json&v=6', 17 | token: '' 18 | }) 19 | 20 | mut app := App{ 21 | c: c, 22 | owners: ["215143736114544640"] 23 | } 24 | 25 | c.subscribe('on_ready', on_ready) 26 | c.subscribe_method('on_message_create', on_message_create, app) 27 | c.connect() 28 | } 29 | 30 | fn on_ready(c &viscord.Connection, packet &viscord.DiscordPacket) { 31 | println("Bot is ready.") 32 | } 33 | 34 | // smaller structs without unnecesary information for faster decoding 35 | struct UserPart { 36 | id string 37 | } 38 | struct MessagePart { 39 | id string 40 | channel_id string 41 | guild_id string 42 | content string 43 | author UserPart 44 | timestamp string 45 | mentions []UserPart 46 | } 47 | 48 | fn on_message_create(app mut App, c viscord.Connection, packet &viscord.DiscordPacket) { 49 | // for some reason c is corrupted so use the connection provided by app instead. 50 | 51 | message := json.decode(MessagePart, packet.d) or { 52 | println('failed to parse message') 53 | return 54 | } 55 | 56 | unix := discord.parse_timestamp(message.timestamp).unix 57 | 58 | if app.mutes[message.author.id] == true { 59 | if unix - app.mutestamps[message.author.id].last() > 10 { 60 | app.mutes[message.author.id] = false 61 | } else { 62 | app.c.delete_message(message.channel_id, message.id) or {} 63 | } 64 | } 65 | 66 | mut arr := app.timestamps[message.author.id] 67 | if arr.len >= 5 { 68 | arr[0] = arr[1] 69 | arr[1] = arr[2] 70 | arr[2] = arr[3] 71 | arr[3] = arr[4] 72 | arr[4] = unix 73 | } else { 74 | arr << unix 75 | } 76 | app.timestamps[message.author.id] = arr 77 | 78 | if arr.len >= 5 && unix - arr[0] < 120 { 79 | app.mutes[message.author.id] = true 80 | mut marr := app.mutestamps[message.author.id] 81 | marr << unix 82 | app.mutestamps[message.author.id] = marr 83 | if marr.len >= 3 && unix - marr[0] < 1800 { 84 | app.c.ban_member(message.guild_id, message.author.id, viscord.RestBan{ 85 | reason: 'auto - spam', 86 | delete_message_days: 1 87 | }) or {} 88 | } 89 | return 90 | } 91 | 92 | if app.owners.index(message.author.id) > -1 { 93 | if message.content.starts_with('vis unmute') { 94 | if message.mentions.len == 0 { 95 | app.c.send_message(message.channel_id, viscord.RestMessage{ 96 | content: "", 97 | _embed: discord.Embed { 98 | description: "🚫 You didn't mention anyone to unmute!", 99 | color: 13060682 100 | } 101 | }) or {} 102 | return 103 | } 104 | app.mutes[message.mentions[0].id] = false 105 | mut marr := app.mutestamps[message.mentions[0].id] 106 | if marr.len > 0 { 107 | marr = marr[0..marr.len-1] 108 | app.mutestamps[message.mentions[0].id] = marr 109 | } 110 | app.c.send_message(message.channel_id, viscord.RestMessage{ 111 | content: "", 112 | _embed: discord.Embed { 113 | description: "✅ Unmuted <@${message.mentions[0].id}>", 114 | color: 4900682 115 | } 116 | }) or {} 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /viscord/connection.v: -------------------------------------------------------------------------------- 1 | module viscord 2 | import net.websocket 3 | import eventbus 4 | import time 5 | 6 | pub struct Connection { 7 | gateway string 8 | pub: 9 | token string 10 | events &eventbus.EventBus 11 | op_events &eventbus.EventBus 12 | mut: 13 | ws &websocket.Client 14 | session_id string 15 | sequence int 16 | heartbeat_interval int 17 | last_heartbeat int 18 | } 19 | 20 | pub struct ConnectionConfig { 21 | gateway string 22 | token string 23 | } 24 | 25 | pub fn new_connection(c ConnectionConfig) &Connection { 26 | mut d := &Connection { 27 | gateway: c.gateway, 28 | token: c.token, 29 | ws: &websocket.new(c.gateway) 30 | events: eventbus.new() 31 | op_events: eventbus.new() 32 | } 33 | 34 | d.ws.subscriber.subscribe_method('on_open', on_open, d) 35 | d.ws.subscriber.subscribe_method('on_message', on_message, d) 36 | d.ws.subscriber.subscribe_method('on_error', on_error, d) 37 | d.ws.subscriber.subscribe_method('on_close', on_close, d) 38 | 39 | d.op_events.subscriber.subscribe_method('on_hello', on_hello, d) 40 | d.op_events.subscriber.subscribe_method('on_dispatch', on_dispatch, d) 41 | 42 | d.events.subscriber.subscribe_method('on_ready', on_ready, d) 43 | 44 | return d 45 | } 46 | 47 | pub fn (d mut Connection) connect () { 48 | mut ws := d.ws 49 | ws.connect() 50 | go ws.listen() 51 | for true { 52 | sleep(1) 53 | if time.now().unix - d.last_heartbeat > d.heartbeat_interval { 54 | heartbeat := HeartbeatPacket { 55 | op: Op.heartbeat, 56 | d: d.sequence 57 | }.encode() 58 | println("HEARTBEAT $heartbeat") 59 | d.ws.write(heartbeat.str, heartbeat.len, .text_frame) 60 | d.last_heartbeat = time.now().unix 61 | } 62 | } 63 | } 64 | 65 | pub fn (d mut Connection) subscribe(name string, handler eventbus.EventHandlerFn) { 66 | d.events.subscriber.subscribe(name, handler) 67 | } 68 | 69 | pub fn (d mut Connection) subscribe_method(name string, handler eventbus.EventHandlerFn, receiver voidptr) { 70 | d.events.subscriber.subscribe_method(name, handler, receiver) 71 | } 72 | 73 | fn on_open(d mut Connection, ws websocket.Client, _ voidptr) { 74 | println('websocket opened.') 75 | } 76 | 77 | fn on_message(d mut Connection, ws websocket.Client, msg &websocket.Message) { 78 | match msg.opcode { 79 | .text_frame { 80 | packet := decode_packet(string(byteptr(msg.payload))) or { 81 | println(err) 82 | return 83 | } 84 | d.sequence = packet.sequence 85 | match Op(packet.op) { 86 | .dispatch { d.op_events.publish('on_dispatch', &ws, &packet) } 87 | .hello { d.op_events.publish('on_hello', &ws, &packet) } 88 | else {} 89 | } 90 | } 91 | else { 92 | println("unhandled opcode") 93 | } 94 | } 95 | } 96 | 97 | fn on_dispatch(d mut Connection, ws websocket.Client, packet &DiscordPacket) { 98 | event := packet.event.to_lower() 99 | d.events.publish('on_$event', d, packet) 100 | } 101 | 102 | fn on_hello(d mut Connection, ws websocket.Client, packet &DiscordPacket) { 103 | hello_data := decode_hello_packet(packet.d) or { 104 | println(err) 105 | return 106 | } 107 | d.heartbeat_interval = hello_data.heartbeat_interval/1000 108 | d.last_heartbeat = time.now().unix 109 | identify_packet := IdentifyPacket{ 110 | token: d.token, 111 | properties: IdentifyPacketProperties{ 112 | os: 'linux', 113 | browser: 'viscord', 114 | device: 'viscord' 115 | }, 116 | shard: [0,1], 117 | guild_subscriptions: true 118 | } 119 | encoded := identify_packet.encode() 120 | //println(encoded) 121 | d.ws.write(encoded.str, encoded.len, .text_frame) 122 | 123 | } 124 | 125 | fn on_ready(d mut Connection, ws websocket.Client, packet &DiscordPacket) { 126 | ready_packet := decode_ready_packet(packet.d) or { return } 127 | d.session_id = ready_packet.session_id 128 | } 129 | 130 | fn on_close(d mut Connection, ws websocket.Client, _ voidptr) { 131 | println('websocket closed.') 132 | } 133 | 134 | fn on_error(d mut Connection, ws websocket.Client, err string) { 135 | println('we have an error.') 136 | println(err) 137 | } 138 | -------------------------------------------------------------------------------- /viscord/packets.v: -------------------------------------------------------------------------------- 1 | module viscord 2 | import json 3 | import discord 4 | 5 | enum Op { 6 | dispatch 7 | heartbeat 8 | identify 9 | presence_update 10 | voice_state_update 11 | five_undocumented 12 | resume 13 | reconnect 14 | request_guild_members 15 | invalid_session 16 | hello 17 | heartbeat_ack 18 | } 19 | 20 | pub struct DiscordPacket { 21 | pub: 22 | op int 23 | sequence int [json:s] 24 | event string [json:t] 25 | d string [raw] 26 | } 27 | 28 | struct HelloPacket { 29 | heartbeat_interval int 30 | } 31 | pub fn decode_hello_packet(s string) ?HelloPacket { 32 | packet := json.decode(HelloPacket, s) or { return error(err) } 33 | return packet 34 | } 35 | 36 | struct IdentifyPacketProperties { 37 | os string [json:"\$os"] 38 | browser string [json:"\$browser"] 39 | device string [json:"\$device"] 40 | } 41 | struct IdentifyPacket { 42 | token string 43 | properties IdentifyPacketProperties 44 | compress bool = false 45 | large_threshold int = 250 46 | shard []int = [0, 1] 47 | presence ?discord.Status 48 | guild_subscriptions bool = true 49 | } 50 | struct OutboundIdentifyPacket { 51 | op int 52 | d IdentifyPacket 53 | } 54 | pub fn (p IdentifyPacket) encode() string { 55 | return json.encode(OutboundIdentifyPacket { 56 | op: int(Op.identify), 57 | d: p 58 | }) 59 | } 60 | 61 | struct ReadyPacket { 62 | v int 63 | private_channels []string 64 | guilds []discord.UnavailableGuild 65 | session_id string 66 | shard []int 67 | } 68 | pub fn decode_ready_packet(s string) ?ReadyPacket { 69 | packet := json.decode(ReadyPacket, s) or { return error(err) } 70 | return packet 71 | } 72 | 73 | struct HeartbeatPacket { 74 | op int 75 | d int 76 | } 77 | pub fn (p HeartbeatPacket) encode() string { 78 | return json.encode(p) 79 | } 80 | 81 | pub fn decode_message_packet(s string) ?discord.Message { 82 | packet := json.decode(discord.Message, s) or { return error(err) } 83 | return packet 84 | } 85 | 86 | pub fn decode_packet(s string) ?DiscordPacket { 87 | packet := json.decode(DiscordPacket, s) or { return error(err) } 88 | return packet 89 | } 90 | -------------------------------------------------------------------------------- /viscord/rest.v: -------------------------------------------------------------------------------- 1 | module viscord 2 | import net.http 3 | import discord 4 | import json 5 | 6 | pub struct RestMessage { 7 | content string 8 | _embed discord.Embed [json:"embed"] 9 | } 10 | pub fn (c Connection) send_message(cid string, m RestMessage) ?http.Response { 11 | return c.post("channels/${cid}/messages", json.encode(m)) 12 | } 13 | pub fn (c Connection) delete_message(cid string, mid string) ?http.Response { 14 | return c.delete("channels/${cid}/messages/${mid}") 15 | } 16 | 17 | pub struct RestBan { 18 | reason string 19 | delete_message_days int [json:'delete-message-days'] 20 | } 21 | pub fn (c Connection) ban_member(gid string, uid string, b RestBan) ?http.Response { 22 | return c.put("guilds/${gid}/bans/${uid}", json.encode(b)) 23 | } 24 | 25 | fn (c Connection) post(p string, data string) ?http.Response { 26 | headers := { 27 | "authorization": "Bot $c.token", 28 | "content-type": "application/json" 29 | } 30 | 31 | res := http.fetch("https://discordapp.com/api/v6/$p", http.FetchConfig{ 32 | method: "post", 33 | headers: headers, 34 | data: data 35 | }) 36 | return res 37 | } 38 | 39 | fn (c Connection) delete(p string) ?http.Response { 40 | headers := { 41 | "authorization": "Bot $c.token", 42 | "content-type": 'application/json' 43 | } 44 | 45 | res := http.fetch("https://discordapp.com/api/v6/$p", http.FetchConfig{ 46 | method: "delete", 47 | headers: headers 48 | }) 49 | return res 50 | } 51 | 52 | fn (c Connection) put(p string, data string) ?http.Response { 53 | headers := { 54 | "authorization": "Bot $c.token", 55 | "content-type": 'application/json' 56 | } 57 | 58 | res := http.fetch("https://discordapp.com/api/v6/$p", http.FetchConfig{ 59 | method: "put", 60 | headers: headers, 61 | data: data 62 | }) 63 | return res 64 | } --------------------------------------------------------------------------------