├── .gitignore ├── BButton ├── BButton.h ├── BButton.m ├── NSString+FontAwesome.h ├── NSString+FontAwesome.m ├── UIColor+BButton.h ├── UIColor+BButton.m └── resources │ └── FontAwesome.ttf ├── DCChatTableCell.xib ├── Default-568h@2x.png ├── Default.png ├── Default@2x.png ├── Discord Classic.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── trevir.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings ├── trevir.mode1v3 ├── trevir.pbxuser └── xcuserdata │ └── Trevir.xcuserdatad │ └── xcschemes │ ├── Discord.xcscheme │ └── xcschememanagement.plist ├── Discord Classic ├── DCAppDelegate.h ├── DCAppDelegate.m ├── DCChannel.h ├── DCChannel.m ├── DCChannelListViewController.h ├── DCChannelListViewController.m ├── DCChatTableCell.h ├── DCChatTableCell.m ├── DCChatViewController.h ├── DCChatViewController.m ├── DCGuild.h ├── DCGuild.m ├── DCGuildListViewController.h ├── DCGuildListViewController.m ├── DCImageViewController.h ├── DCImageViewController.m ├── DCInfoPageViewController.h ├── DCInfoPageViewController.m ├── DCMessage.h ├── DCMessage.m ├── DCServerCommunicator.h ├── DCServerCommunicator.m ├── DCSettingsViewController.h ├── DCSettingsViewController.m ├── DCTools.h ├── DCTools.m ├── DCUser.h ├── DCUser.m ├── DCViewController.h ├── DCViewController.m ├── DCViewController.xib ├── DCWelcomeViewController.h ├── DCWelcomeViewController.m ├── Discord Classic-Info.plist ├── Discord Classic-Prefix.pch ├── TRMalleableFrameView.h ├── TRMalleableFrameView.m ├── en.lproj │ └── InfoPlist.strings └── main.m ├── Icon.png ├── Icon@2x.png ├── LICENSE ├── README.md ├── Screenshot.png ├── Storyboard.storyboard ├── UINavigationBarTexture.png ├── UINavigationBarTexture@2x.png └── Websocket ├── NSString+Base64.h ├── NSString+Base64.m ├── WSFrame.h ├── WSFrame.m ├── WSMessage.h ├── WSMessage.m ├── WSMessageProcessor.h ├── WSMessageProcessor.m ├── WSWebSocket-Prefix.pch ├── WSWebSocket.h └── WSWebSocket.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | # General 26 | .DS_Store 27 | .AppleDouble 28 | .LSOverride 29 | 30 | # Icon must end with two \r 31 | Icon 32 | 33 | 34 | # Thumbnails 35 | ._* 36 | 37 | # Files that might appear in the root of a volume 38 | .DocumentRevisions-V100 39 | .fseventsd 40 | .Spotlight-V100 41 | .TemporaryItems 42 | .Trashes 43 | .VolumeIcon.icns 44 | .com.apple.timemachine.donotpresent 45 | 46 | # Directories potentially created on remote AFP share 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk -------------------------------------------------------------------------------- /BButton/BButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // BButton.h 3 | // 4 | // Created by Mathieu Bolard on 31/07/12. 5 | // Copyright (c) 2012 Mathieu Bolard. All rights reserved. 6 | // 7 | // https://github.com/mattlawer/BButton 8 | // 9 | // Redistribution and use in source and binary forms, with or without modification, 10 | // are permitted provided that the following conditions are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // 15 | // * Redistributions in binary form must reproduce the above copyright notice, 16 | // this list of conditions and the following disclaimer in the documentation 17 | // and/or other materials provided with the distribution. 18 | // 19 | // * Neither the name of Mathieu Bolard, mattlawer nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 24 | // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | // IN NO EVENT SHALL Mathieu Bolard BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | // ----------------------------------------- 31 | // Edited and refactored by Jesse Squires on 2 April, 2013. 32 | // 33 | // http://github.com/jessesquires/BButton 34 | // 35 | // http://hexedbits.com 36 | // 37 | 38 | #import 39 | #import "UIColor+BButton.h" 40 | #import "NSString+FontAwesome.h" 41 | 42 | typedef enum { 43 | BButtonTypeDefault = 0, 44 | BButtonTypePrimary, 45 | BButtonTypeInfo, 46 | BButtonTypeSuccess, 47 | BButtonTypeWarning, 48 | BButtonTypeDanger, 49 | BButtonTypeInverse, 50 | BButtonTypeTwitter, 51 | BButtonTypeFacebook, 52 | BButtonTypePurple, 53 | BButtonTypeGray 54 | } BButtonType; 55 | 56 | 57 | @interface BButton : UIButton 58 | 59 | @property (strong, nonatomic) UIColor *color; 60 | @property (assign, nonatomic) BOOL shouldShowDisabled; 61 | 62 | #pragma mark - Initialization 63 | - (id)initWithFrame:(CGRect)frame type:(BButtonType)type; 64 | - (id)initWithFrame:(CGRect)frame type:(BButtonType)type icon:(FAIcon)icon fontSize:(CGFloat)fontSize; 65 | 66 | - (id)initWithFrame:(CGRect)frame color:(UIColor *)aColor; 67 | - (id)initWithFrame:(CGRect)frame color:(UIColor *)aColor icon:(FAIcon)icon fontSize:(CGFloat)fontSize; 68 | 69 | + (BButton *)awesomeButtonWithOnlyIcon:(FAIcon)icon type:(BButtonType)type; 70 | + (BButton *)awesomeButtonWithOnlyIcon:(FAIcon)icon color:(UIColor *)color; 71 | 72 | #pragma mark - BButton 73 | - (void)setType:(BButtonType)type; 74 | - (void)addAwesomeIcon:(FAIcon)icon beforeTitle:(BOOL)before; 75 | 76 | @end -------------------------------------------------------------------------------- /BButton/BButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // BButton.m 3 | // 4 | // Created by Mathieu Bolard on 31/07/12. 5 | // Copyright (c) 2012 Mathieu Bolard. All rights reserved. 6 | // 7 | // https://github.com/mattlawer/BButton 8 | // 9 | // Redistribution and use in source and binary forms, with or without modification, 10 | // are permitted provided that the following conditions are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // 15 | // * Redistributions in binary form must reproduce the above copyright notice, 16 | // this list of conditions and the following disclaimer in the documentation 17 | // and/or other materials provided with the distribution. 18 | // 19 | // * Neither the name of Mathieu Bolard, mattlawer nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 24 | // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | // IN NO EVENT SHALL Mathieu Bolard BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | // ----------------------------------------- 31 | // Edited and refactored by Jesse Squires on 2 April, 2013. 32 | // 33 | // http://github.com/jessesquires/BButton 34 | // 35 | // http://hexedbits.com 36 | // 37 | 38 | #import "BButton.h" 39 | #import 40 | 41 | @interface BButton () 42 | 43 | @property (assign, nonatomic) CGGradientRef gradient; 44 | @property (readonly, nonatomic) UILabel* fastTitleLabel; 45 | 46 | - (void)setup; 47 | + (UIColor *)colorForButtonType:(BButtonType)type; 48 | - (void)setGradientEnabled:(BOOL)enabled; 49 | 50 | @end 51 | 52 | 53 | 54 | @implementation BButton 55 | 56 | @synthesize color; 57 | @synthesize gradient; 58 | @synthesize shouldShowDisabled; 59 | 60 | #pragma mark - Initialization 61 | - (void)setup 62 | { 63 | self.backgroundColor = [UIColor clearColor]; 64 | self.fastTitleLabel.shadowOffset = CGSizeMake(0.0f, -1.0f); 65 | self.fastTitleLabel.font = [UIFont boldSystemFontOfSize:17.0f]; 66 | self.shouldShowDisabled = NO; 67 | [self setType:BButtonTypeDefault]; 68 | } 69 | 70 | - (id)initWithFrame:(CGRect)frame type:(BButtonType)type 71 | { 72 | return [self initWithFrame:frame color:[BButton colorForButtonType:type]]; 73 | } 74 | 75 | - (id)initWithFrame:(CGRect)frame type:(BButtonType)type icon:(FAIcon)icon fontSize:(CGFloat)fontSize 76 | { 77 | return [self initWithFrame:frame 78 | color:[BButton colorForButtonType:type] 79 | icon:icon 80 | fontSize:fontSize]; 81 | } 82 | 83 | - (id)initWithFrame:(CGRect)frame color:(UIColor *)aColor 84 | { 85 | self = [self initWithFrame:frame]; 86 | if(self) { 87 | self.color = aColor; 88 | } 89 | return self; 90 | } 91 | 92 | - (id)initWithFrame:(CGRect)frame color:(UIColor *)aColor icon:(FAIcon)icon fontSize:(CGFloat)fontSize 93 | { 94 | self = [self initWithFrame:frame color:aColor]; 95 | if(self) { 96 | self.fastTitleLabel.font = [UIFont fontWithName:@"FontAwesome" size:fontSize]; 97 | self.fastTitleLabel.textAlignment = NSTextAlignmentCenter; 98 | [self setTitle:[NSString stringFromAwesomeIcon:icon] forState:UIControlStateNormal]; 99 | } 100 | return self; 101 | } 102 | 103 | - (id)initWithFrame:(CGRect)frame 104 | { 105 | self = [super initWithFrame:frame]; 106 | if(self) { 107 | [self setup]; 108 | } 109 | return self; 110 | } 111 | 112 | - (id)initWithCoder:(NSCoder *)aDecoder 113 | { 114 | self = [super initWithCoder:aDecoder]; 115 | if(self) { 116 | [self setup]; 117 | } 118 | return self; 119 | } 120 | 121 | - (id)init 122 | { 123 | self = [super init]; 124 | if(self) { 125 | [self setup]; 126 | } 127 | return self; 128 | } 129 | 130 | + (BButton *)awesomeButtonWithOnlyIcon:(FAIcon)icon type:(BButtonType)type 131 | { 132 | return [BButton awesomeButtonWithOnlyIcon:icon 133 | color:[BButton colorForButtonType:type]]; 134 | } 135 | 136 | + (BButton *)awesomeButtonWithOnlyIcon:(FAIcon)icon color:(UIColor *)color 137 | { 138 | return [[BButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 40.0f) 139 | color:color 140 | icon:icon 141 | fontSize:20.0f]; 142 | } 143 | 144 | #pragma mark - Parent overrides 145 | - (void)setHighlighted:(BOOL)highlighted 146 | { 147 | [super setHighlighted:highlighted]; 148 | [self setNeedsDisplay]; 149 | } 150 | 151 | - (void)setEnabled:(BOOL)enabled 152 | { 153 | [super setEnabled:enabled]; 154 | 155 | if(self.shouldShowDisabled) 156 | [self setGradientEnabled:enabled]; 157 | 158 | [self setNeedsDisplay]; 159 | } 160 | 161 | #pragma mark - Setters 162 | - (void)setColor:(UIColor *)newColor 163 | { 164 | color = newColor; 165 | 166 | if([newColor isLightColor]) { 167 | [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 168 | [self setTitleShadowColor:[[UIColor whiteColor] colorWithAlphaComponent:0.6f] forState:UIControlStateNormal]; 169 | 170 | if(self.shouldShowDisabled) 171 | [self setTitleColor:[UIColor colorWithWhite:0.4f alpha:0.5f] forState:UIControlStateDisabled]; 172 | } 173 | else { 174 | [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 175 | [self setTitleShadowColor:[[UIColor blackColor] colorWithAlphaComponent:0.6f] forState:UIControlStateNormal]; 176 | 177 | if(self.shouldShowDisabled) 178 | [self setTitleColor:[UIColor colorWithWhite:1.0f alpha:0.5f] forState:UIControlStateDisabled]; 179 | } 180 | 181 | if(self.shouldShowDisabled) 182 | [self setGradientEnabled:self.enabled]; 183 | else 184 | [self setGradientEnabled:YES]; 185 | 186 | [self setNeedsDisplay]; 187 | } 188 | 189 | - (void)setShouldShowDisabled:(BOOL)show 190 | { 191 | shouldShowDisabled = show; 192 | 193 | if(show) { 194 | if([self.color isLightColor]) 195 | [self setTitleColor:[UIColor colorWithWhite:0.4f alpha:0.5f] forState:UIControlStateDisabled]; 196 | else 197 | [self setTitleColor:[UIColor colorWithWhite:1.0f alpha:0.5f] forState:UIControlStateDisabled]; 198 | } 199 | else { 200 | if([self.color isLightColor]) 201 | [self setTitleColor:[UIColor blackColor] forState:UIControlStateDisabled]; 202 | else 203 | [self setTitleColor:[UIColor whiteColor] forState:UIControlStateDisabled]; 204 | } 205 | } 206 | 207 | #pragma mark - BButton 208 | - (void)setType:(BButtonType)type 209 | { 210 | self.color = [BButton colorForButtonType:type]; 211 | } 212 | 213 | - (void)addAwesomeIcon:(FAIcon)icon beforeTitle:(BOOL)before 214 | { 215 | NSString *iconString = [NSString stringFromAwesomeIcon:icon]; 216 | self.fastTitleLabel.font = [UIFont fontWithName:@"FontAwesome" 217 | size:self.fastTitleLabel.font.pointSize]; 218 | 219 | NSString *title = [NSString stringWithFormat:@"%@", iconString]; 220 | 221 | // Need to get current title as like this, because self.fastTitleLabel.text does not always give us the current title 222 | NSString * currentTitle = ![self.titleLabel.text isEmpty] ? self.titleLabel.text : self.fastTitleLabel.text; 223 | if(![currentTitle isEmpty]) { 224 | if(before) { 225 | title = [NSString stringWithFormat:@"%@ %@", iconString, currentTitle]; 226 | } 227 | else { 228 | title = [NSString stringWithFormat:@"%@ %@", currentTitle, iconString]; 229 | } 230 | } 231 | 232 | [self setTitle:title forState:UIControlStateNormal]; 233 | } 234 | 235 | + (UIColor *)colorForButtonType:(BButtonType)type 236 | { 237 | UIColor *newColor = nil; 238 | 239 | switch (type) { 240 | case BButtonTypePrimary: 241 | newColor = [UIColor colorWithRed:0.00f green:0.33f blue:0.80f alpha:1.00f]; 242 | break; 243 | case BButtonTypeInfo: 244 | newColor = [UIColor colorWithRed:0.18f green:0.59f blue:0.71f alpha:1.00f]; 245 | break; 246 | case BButtonTypeSuccess: 247 | newColor = [UIColor colorWithRed:0.32f green:0.64f blue:0.32f alpha:1.00f]; 248 | break; 249 | case BButtonTypeWarning: 250 | newColor = [UIColor colorWithRed:0.97f green:0.58f blue:0.02f alpha:1.00f]; 251 | break; 252 | case BButtonTypeDanger: 253 | newColor = [UIColor colorWithRed:0.74f green:0.21f blue:0.18f alpha:1.00f]; 254 | break; 255 | case BButtonTypeInverse: 256 | newColor = [UIColor colorWithRed:0.13f green:0.13f blue:0.13f alpha:1.00f]; 257 | break; 258 | case BButtonTypeTwitter: 259 | newColor = [UIColor colorWithRed:0.25f green:0.60f blue:1.00f alpha:1.00f]; 260 | break; 261 | case BButtonTypeFacebook: 262 | newColor = [UIColor colorWithRed:0.23f green:0.35f blue:0.60f alpha:1.00f]; 263 | break; 264 | case BButtonTypePurple: 265 | newColor = [UIColor colorWithRed:0.45f green:0.30f blue:0.75f alpha:1.00f]; 266 | break; 267 | case BButtonTypeGray: 268 | newColor = [UIColor colorWithRed:0.60f green:0.60f blue:0.60f alpha:1.00f]; 269 | break; 270 | case BButtonTypeDefault: 271 | default: 272 | newColor = [UIColor colorWithRed:0.85f green:0.85f blue:0.85f alpha:1.00f]; 273 | break; 274 | } 275 | 276 | return newColor; 277 | } 278 | 279 | #pragma mark - Drawing 280 | - (void)drawRect:(CGRect)rect 281 | { 282 | [super drawRect:rect]; 283 | CGContextRef context = UIGraphicsGetCurrentContext(); 284 | 285 | UIColor *border = [self.color darkenColorWithValue:0.06f]; 286 | 287 | // Shadow Declarations 288 | UIColor *shadow = [self.color lightenColorWithValue:0.50f]; 289 | CGSize shadowOffset = CGSizeMake(0.0f, 1.0f); 290 | CGFloat shadowBlurRadius = 2.0f; 291 | 292 | // Rounded Rectangle Drawing 293 | UIBezierPath *roundedRectanglePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0.5f, 0.5f, rect.size.width-1.0f, rect.size.height-1.0f) 294 | cornerRadius:6.0f]; 295 | 296 | CGContextSaveGState(context); 297 | 298 | [roundedRectanglePath addClip]; 299 | 300 | CGContextDrawLinearGradient(context, 301 | self.gradient, 302 | CGPointMake(0.0f, self.highlighted ? rect.size.height - 0.5f : 0.5f), 303 | CGPointMake(0.0f, self.highlighted ? 0.5f : rect.size.height - 0.5f), 0.0f); 304 | 305 | CGContextRestoreGState(context); 306 | 307 | if(!self.highlighted) { 308 | // Rounded Rectangle Inner Shadow 309 | CGRect roundedRectangleBorderRect = CGRectInset([roundedRectanglePath bounds], -shadowBlurRadius, -shadowBlurRadius); 310 | roundedRectangleBorderRect = CGRectOffset(roundedRectangleBorderRect, -shadowOffset.width, -shadowOffset.height); 311 | roundedRectangleBorderRect = CGRectInset(CGRectUnion(roundedRectangleBorderRect, [roundedRectanglePath bounds]), -1.0f, -1.0f); 312 | 313 | UIBezierPath *roundedRectangleNegativePath = [UIBezierPath bezierPathWithRect: roundedRectangleBorderRect]; 314 | [roundedRectangleNegativePath appendPath: roundedRectanglePath]; 315 | roundedRectangleNegativePath.usesEvenOddFillRule = YES; 316 | 317 | CGContextSaveGState(context); 318 | { 319 | CGFloat xOffset = shadowOffset.width + round(roundedRectangleBorderRect.size.width); 320 | CGFloat yOffset = shadowOffset.height; 321 | CGContextSetShadowWithColor(context, 322 | CGSizeMake(xOffset + copysign(0.1f, xOffset), yOffset + copysign(0.1f, yOffset)), 323 | shadowBlurRadius, 324 | shadow.CGColor); 325 | 326 | [roundedRectanglePath addClip]; 327 | CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(roundedRectangleBorderRect.size.width), 0.0f); 328 | [roundedRectangleNegativePath applyTransform: transform]; 329 | [[UIColor grayColor] setFill]; 330 | [roundedRectangleNegativePath fill]; 331 | } 332 | CGContextRestoreGState(context); 333 | } 334 | 335 | [border setStroke]; 336 | roundedRectanglePath.lineWidth = 1.0f; 337 | [roundedRectanglePath stroke]; 338 | } 339 | 340 | - (void)setGradientEnabled:(BOOL)enabled 341 | { 342 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 343 | UIColor *topColor = enabled ? [self.color lightenColorWithValue:0.12f] : [self.color darkenColorWithValue:0.12f]; 344 | 345 | NSArray *newGradientColors = [NSArray arrayWithObjects:(id)topColor.CGColor, (id)self.color.CGColor, nil]; 346 | CGFloat newGradientLocations[] = {0.0f, 1.0f}; 347 | 348 | gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)newGradientColors, newGradientLocations); 349 | CGColorSpaceRelease(colorSpace); 350 | } 351 | 352 | #pragma mark - performance 353 | 354 | - (UILabel*)fastTitleLabel 355 | { 356 | return (self.subviews.count == 1) ? self.subviews[0] : self.titleLabel; 357 | } 358 | 359 | @end 360 | -------------------------------------------------------------------------------- /BButton/NSString+FontAwesome.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+FontAwesome.h 3 | // 4 | // Created by Pit Garbe on 27.09.12. 5 | // Updated to Font Awesome 3.1.1 on 17.05.2013. 6 | // Copyright (c) 2012 Pit Garbe. All rights reserved. 7 | // 8 | // https://github.com/leberwurstsaft/FontAwesome-for-iOS 9 | // 10 | // 11 | // * The Font Awesome font is licensed under the SIL Open Font License 12 | // http://scripts.sil.org/OFL 13 | // 14 | // 15 | // * Font Awesome CSS, LESS, and SASS files are licensed under the MIT License 16 | // http://opensource.org/licenses/mit-license.html 17 | // 18 | // 19 | // * The Font Awesome pictograms are licensed under the CC BY 3.0 License 20 | // http://creativecommons.org/licenses/by/3.0 21 | // 22 | // 23 | // * Attribution is no longer required in Font Awesome 3.0, but much appreciated: 24 | // "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" 25 | // 26 | // 27 | // ----------------------------------------- 28 | // Edited and refactored by Jesse Squires on 2 April, 2013. 29 | // 30 | // http://github.com/jessesquires/BButton 31 | // 32 | // http://hexedbits.com 33 | // 34 | 35 | #import 36 | 37 | typedef enum { 38 | FAIconGlass = 0, 39 | FAIconMusic, 40 | FAIconSearch, 41 | FAIconEnvelope, 42 | FAIconHeart, 43 | FAIconStar, 44 | FAIconStarEmpty, 45 | FAIconUser, 46 | FAIconFilm, 47 | FAIconThLarge, 48 | FAIconTh, 49 | FAIconThList, 50 | FAIconOk, 51 | FAIconRemove, 52 | FAIconZoomIn, 53 | FAIconZoomOut, 54 | FAIconOff, 55 | FAIconSignal, 56 | FAIconCog, 57 | FAIconTrash, 58 | FAIconHome, 59 | FAIconFile, 60 | FAIconTime, 61 | FAIconRoad, 62 | FAIconDownloadAlt, 63 | FAIconDownload, 64 | FAIconUpload, 65 | FAIconInbox, 66 | FAIconPlayCircle, 67 | FAIconRepeat, 68 | FAIconRefresh, 69 | FAIconListAlt, 70 | FAIconLock, 71 | FAIconFlag, 72 | FAIconHeadphones, 73 | FAIconVolumeOff, 74 | FAIconVolumeDown, 75 | FAIconVolumeUp, 76 | FAIconQrcode, 77 | FAIconBarcode, 78 | FAIconTag, 79 | FAIconTags, 80 | FAIconBook, 81 | FAIconBookmark, 82 | FAIconPrint, 83 | FAIconCamera, 84 | FAIconFont, 85 | FAIconBold, 86 | FAIconItalic, 87 | FAIconTextHeight, 88 | FAIconTextWidth, 89 | FAIconAlignLeft, 90 | FAIconAlignCenter, 91 | FAIconAlignRight, 92 | FAIconAlignJustify, 93 | FAIconList, 94 | FAIconIndentLeft, 95 | FAIconIndentRight, 96 | FAIconFacetimeVideo, 97 | FAIconPicture, 98 | FAIconPencil, 99 | FAIconMapMarker, 100 | FAIconAdjust, 101 | FAIconTint, 102 | FAIconEdit, 103 | FAIconShare, 104 | FAIconCheck, 105 | FAIconMove, 106 | FAIconStepBackward, 107 | FAIconFastBackward, 108 | FAIconBackward, 109 | FAIconPlay, 110 | FAIconPause, 111 | FAIconStop, 112 | FAIconForward, 113 | FAIconFastForward, 114 | FAIconStepForward, 115 | FAIconEject, 116 | FAIconChevronLeft, 117 | FAIconChevronRight, 118 | FAIconPlusSign, 119 | FAIconMinusSign, 120 | FAIconRemoveSign, 121 | FAIconOkSign, 122 | FAIconQuestionSign, 123 | FAIconInfoSign, 124 | FAIconScreenshot, 125 | FAIconRemoveCircle, 126 | FAIconOkCircle, 127 | FAIconBanCircle, 128 | FAIconArrowLeft, 129 | FAIconArrowRight, 130 | FAIconArrowUp, 131 | FAIconArrowDown, 132 | FAIconShareAlt, 133 | FAIconResizeFull, 134 | FAIconResizeSmall, 135 | FAIconPlus, 136 | FAIconMinus, 137 | FAIconAsterisk, 138 | FAIconExclamationSign, 139 | FAIconGift, 140 | FAIconLeaf, 141 | FAIconFire, 142 | FAIconEyeOpen, 143 | FAIconEyeClose, 144 | FAIconWarningSign, 145 | FAIconPlane, 146 | FAIconCalendar, 147 | FAIconRandom, 148 | FAIconComment, 149 | FAIconMagnet, 150 | FAIconChevronUp, 151 | FAIconChevronDown, 152 | FAIconRetweet, 153 | FAIconShoppingCart, 154 | FAIconFolderClose, 155 | FAIconFolderOpen, 156 | FAIconResizeVertical, 157 | FAIconResizeHorizontal, 158 | FAIconBarChart, 159 | FAIconTwitterSign, 160 | FAIconFacebookSign, 161 | FAIconCameraRetro, 162 | FAIconKey, 163 | FAIconCogs, 164 | FAIconComments, 165 | FAIconThumbsUp, 166 | FAIconThumbsDown, 167 | FAIconStarHalf, 168 | FAIconHeartEmpty, 169 | FAIconSignout, 170 | FAIconLinkedinSign, 171 | FAIconPushpin, 172 | FAIconExternalLink, 173 | FAIconSignin, 174 | FAIconTrophy, 175 | FAIconGithubSign, 176 | FAIconUploadAlt, 177 | FAIconLemon, 178 | FAIconPhone, 179 | FAIconCheckEmpty, 180 | FAIconBookmarkEmpty, 181 | FAIconPhoneSign, 182 | FAIconTwitter, 183 | FAIconFacebook, 184 | FAIconGithub, 185 | FAIconUnlock, 186 | FAIconCreditCard, 187 | FAIconRss, 188 | FAIconHdd, 189 | FAIconBullhorn, 190 | FAIconBell, 191 | FAIconCertificate, 192 | FAIconHandRight, 193 | FAIconHandLeft, 194 | FAIconHandUp, 195 | FAIconHandDown, 196 | FAIconCircleArrowLeft, 197 | FAIconCircleArrowRight, 198 | FAIconCircleArrowUp, 199 | FAIconCircleArrowDown, 200 | FAIconGlobe, 201 | FAIconWrench, 202 | FAIconTasks, 203 | FAIconFilter, 204 | FAIconBriefcase, 205 | FAIconFullscreen, 206 | FAIconGroup, 207 | FAIconLink, 208 | FAIconCloud, 209 | FAIconBeaker, 210 | FAIconCut, 211 | FAIconCopy, 212 | FAIconPaperClip, 213 | FAIconSave, 214 | FAIconSignBlank, 215 | FAIconReorder, 216 | FAIconListUl, 217 | FAIconListOl, 218 | FAIconStrikethrough, 219 | FAIconUnderline, 220 | FAIconTable, 221 | FAIconMagic, 222 | FAIconTruck, 223 | FAIconPinterest, 224 | FAIconPinterestSign, 225 | FAIconGooglePlusSign, 226 | FAIconGooglePlus, 227 | FAIconMoney, 228 | FAIconCaretDown, 229 | FAIconCaretUp, 230 | FAIconCaretLeft, 231 | FAIconCaretRight, 232 | FAIconColumns, 233 | FAIconSort, 234 | FAIconSortDown, 235 | FAIconSortUp, 236 | FAIconEnvelopeAlt, 237 | FAIconLinkedin, 238 | FAIconUndo, 239 | FAIconLegal, 240 | FAIconDashboard, 241 | FAIconCommentAlt, 242 | FAIconCommentsAlt, 243 | FAIconBolt, 244 | FAIconSitemap, 245 | FAIconUmbrella, 246 | FAIconPaste, 247 | FAIconLightBulb, 248 | FAIconExchange, 249 | FAIconCloudDownload, 250 | FAIconCloudUpload, 251 | FAIconUserMd, 252 | FAIconStethoscope, 253 | FAIconSuitecase, 254 | FAIconBellAlt, 255 | FAIconCoffee, 256 | FAIconFood, 257 | FAIconFileAlt, 258 | FAIconBuilding, 259 | FAIconHospital, 260 | FAIconAmbulance, 261 | FAIconMedkit, 262 | FAIconFighterJet, 263 | FAIconBeer, 264 | FAIconHSign, 265 | FAIconPlusSignAlt, 266 | FAIconDoubleAngleLeft, 267 | FAIconDoubleAngleRight, 268 | FAIconDoubleAngleUp, 269 | FAIconDoubleAngleDown, 270 | FAIconAngleLeft, 271 | FAIconAngleRight, 272 | FAIconAngleUp, 273 | FAIconAngleDown, 274 | FAIconDesktop, 275 | FAIconLaptop, 276 | FAIconTablet, 277 | FAIconMobilePhone, 278 | FAIconCircleBlank, 279 | FAIconQuoteLeft, 280 | FAIconQuoteRight, 281 | FAIconSpinner, 282 | FAIconCircle, 283 | FAIconReply, 284 | FAIconFolderCloseAlt, 285 | FAIconFolderOpenAlt, 286 | FAIconExpandAlt, 287 | FAIconCollapseAlt, 288 | FAIconSmile, 289 | FAIconFrown, 290 | FAIconMeh, 291 | FAIconGamepad, 292 | FAIconKeyboard, 293 | FAIconFlagAlt, 294 | FAIconFlagCheckered, 295 | FAIconTerminal, 296 | FAIconCode, 297 | FAIconReplyAll, 298 | FAIconStarHalfAlt, 299 | FAIconLocationArrow, 300 | FAIconCrop, 301 | FAIconCodeFork, 302 | FAIconUnlink, 303 | FAIconQuestion, 304 | FAIconInfo, 305 | FAIconExclamation, 306 | FAIconSuperscript, 307 | FAIconSubscript, 308 | FAIconEraser, 309 | FAIconPuzzlePiece, 310 | FAIconMicrophone, 311 | FAIconMicrophoneOff, 312 | FAIconShield, 313 | FAIconCalendarEmpty, 314 | FAIconFireExtinguisher, 315 | FAIconRocket, 316 | FAIconMaxCDN, 317 | FAIconChevronSignLeft, 318 | FAIconChevronSignRight, 319 | FAIconChevronSignUp, 320 | FAIconChevronSignDown, 321 | FAIconHTML5, 322 | FAIconCSS3, 323 | FAIconFAIconAnchor, 324 | FAIconUnlockAlt, 325 | FAIconBullseye, 326 | FAIconEllipsisHorizontal, 327 | FAIconEllipsisVertical, 328 | FAIconRSS, 329 | FAIconPlaySign, 330 | FAIconTicket, 331 | FAIconMinusSignAlt, 332 | FAIconCheckMinus, 333 | FAIconLevelUp, 334 | FAIconLevelDown, 335 | FAIconCheckSign, 336 | FAIconEditSign, 337 | FAIconExternalLinkSign, 338 | FAIconShareSign 339 | } FAIcon; 340 | 341 | 342 | @interface NSString (FontAwesome) 343 | 344 | + (NSString *)stringFromAwesomeIcon:(FAIcon)icon; 345 | - (NSString *)trimWhitespace; 346 | - (BOOL)isEmpty; 347 | 348 | @end -------------------------------------------------------------------------------- /BButton/NSString+FontAwesome.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+FontAwesome.m 3 | // 4 | // Created by Pit Garbe on 27.09.12. 5 | // Updated to Font Awesome 3.1.1 on 17.05.2013. 6 | // Copyright (c) 2012 Pit Garbe. All rights reserved. 7 | // 8 | // https://github.com/leberwurstsaft/FontAwesome-for-iOS 9 | // 10 | // 11 | // * The Font Awesome font is licensed under the SIL Open Font License 12 | // http://scripts.sil.org/OFL 13 | // 14 | // 15 | // * Font Awesome CSS, LESS, and SASS files are licensed under the MIT License 16 | // http://opensource.org/licenses/mit-license.html 17 | // 18 | // 19 | // * The Font Awesome pictograms are licensed under the CC BY 3.0 License 20 | // http://creativecommons.org/licenses/by/3.0 21 | // 22 | // 23 | // * Attribution is no longer required in Font Awesome 3.0, but much appreciated: 24 | // "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" 25 | // 26 | // 27 | // ----------------------------------------- 28 | // Edited and refactored by Jesse Squires on 2 April, 2013. 29 | // 30 | // http://github.com/jessesquires/BButton 31 | // 32 | // http://hexedbits.com 33 | // 34 | 35 | #import "NSString+FontAwesome.h" 36 | 37 | static const NSArray *awesomeStrings; 38 | 39 | 40 | @implementation NSString (FontAwesome) 41 | 42 | + (NSString *)stringFromAwesomeIcon:(FAIcon)icon 43 | { 44 | if(!awesomeStrings) { 45 | awesomeStrings = [NSArray arrayWithObjects:@"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", @"", nil]; 46 | } 47 | 48 | return [awesomeStrings objectAtIndex:icon]; 49 | } 50 | 51 | - (NSString *)trimWhitespace 52 | { 53 | NSMutableString *str = [self mutableCopy]; 54 | CFStringTrimWhitespace((__bridge CFMutableStringRef)str); 55 | return str; 56 | } 57 | 58 | - (BOOL)isEmpty 59 | { 60 | return [[self trimWhitespace] isEqualToString:@""]; 61 | } 62 | 63 | @end -------------------------------------------------------------------------------- /BButton/UIColor+BButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+BButton.h 3 | // 4 | // Created by Mathieu Bolard on 31/07/12. 5 | // Copyright (c) 2012 Mathieu Bolard. All rights reserved. 6 | // 7 | // https://github.com/mattlawer/BButton 8 | // 9 | // Redistribution and use in source and binary forms, with or without modification, 10 | // are permitted provided that the following conditions are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // 15 | // * Redistributions in binary form must reproduce the above copyright notice, 16 | // this list of conditions and the following disclaimer in the documentation 17 | // and/or other materials provided with the distribution. 18 | // 19 | // * Neither the name of Mathieu Bolard, mattlawer nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 24 | // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | // IN NO EVENT SHALL Mathieu Bolard BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | // ----------------------------------------- 31 | // Edited and refactored by Jesse Squires on 2 April, 2013. 32 | // 33 | // http://github.com/jessesquires/BButton 34 | // 35 | // http://hexedbits.com 36 | // 37 | 38 | #import 39 | 40 | @interface UIColor (BButton) 41 | 42 | - (UIColor *)lightenColorWithValue:(CGFloat)value; 43 | - (UIColor *)darkenColorWithValue:(CGFloat)value; 44 | - (BOOL)isLightColor; 45 | 46 | @end -------------------------------------------------------------------------------- /BButton/UIColor+BButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+BButton.m 3 | // 4 | // Created by Mathieu Bolard on 31/07/12. 5 | // Copyright (c) 2012 Mathieu Bolard. All rights reserved. 6 | // 7 | // https://github.com/mattlawer/BButton 8 | // 9 | // Redistribution and use in source and binary forms, with or without modification, 10 | // are permitted provided that the following conditions are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // 15 | // * Redistributions in binary form must reproduce the above copyright notice, 16 | // this list of conditions and the following disclaimer in the documentation 17 | // and/or other materials provided with the distribution. 18 | // 19 | // * Neither the name of Mathieu Bolard, mattlawer nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 24 | // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | // IN NO EVENT SHALL Mathieu Bolard BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | // ----------------------------------------- 31 | // Edited and refactored by Jesse Squires on 2 April, 2013. 32 | // 33 | // http://github.com/jessesquires/BButton 34 | // 35 | // http://hexedbits.com 36 | // 37 | 38 | #import "UIColor+BButton.h" 39 | 40 | @implementation UIColor (BButton) 41 | 42 | - (UIColor *)lightenColorWithValue:(CGFloat)value 43 | { 44 | size_t totalComponents = CGColorGetNumberOfComponents(self.CGColor); 45 | BOOL isGreyscale = (totalComponents == 2) ? YES : NO; 46 | 47 | CGFloat *oldComponents = (CGFloat *)CGColorGetComponents(self.CGColor); 48 | CGFloat newComponents[4]; 49 | 50 | if(isGreyscale) { 51 | newComponents[0] = oldComponents[0] + value > 1.0 ? 1.0 : oldComponents[0] + value; 52 | newComponents[1] = oldComponents[0] + value > 1.0 ? 1.0 : oldComponents[0] + value; 53 | newComponents[2] = oldComponents[0] + value > 1.0 ? 1.0 : oldComponents[0] + value; 54 | newComponents[3] = oldComponents[1]; 55 | } 56 | else { 57 | newComponents[0] = oldComponents[0] + value > 1.0 ? 1.0 : oldComponents[0] + value; 58 | newComponents[1] = oldComponents[1] + value > 1.0 ? 1.0 : oldComponents[1] + value; 59 | newComponents[2] = oldComponents[2] + value > 1.0 ? 1.0 : oldComponents[2] + value; 60 | newComponents[3] = oldComponents[3]; 61 | } 62 | 63 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 64 | CGColorRef newColor = CGColorCreate(colorSpace, newComponents); 65 | CGColorSpaceRelease(colorSpace); 66 | 67 | UIColor *retColor = [UIColor colorWithCGColor:newColor]; 68 | CGColorRelease(newColor); 69 | 70 | return retColor; 71 | } 72 | 73 | - (UIColor *)darkenColorWithValue:(CGFloat)value 74 | { 75 | size_t totalComponents = CGColorGetNumberOfComponents(self.CGColor); 76 | BOOL isGreyscale = (totalComponents == 2) ? YES : NO; 77 | 78 | CGFloat *oldComponents = (CGFloat *)CGColorGetComponents(self.CGColor); 79 | CGFloat newComponents[4]; 80 | 81 | if(isGreyscale) { 82 | newComponents[0] = oldComponents[0] - value < 0.0 ? 0.0 : oldComponents[0] - value; 83 | newComponents[1] = oldComponents[0] - value < 0.0 ? 0.0 : oldComponents[0] - value; 84 | newComponents[2] = oldComponents[0] - value < 0.0 ? 0.0 : oldComponents[0] - value; 85 | newComponents[3] = oldComponents[1]; 86 | } 87 | else { 88 | newComponents[0] = oldComponents[0] - value < 0.0 ? 0.0 : oldComponents[0] - value; 89 | newComponents[1] = oldComponents[1] - value < 0.0 ? 0.0 : oldComponents[1] - value; 90 | newComponents[2] = oldComponents[2] - value < 0.0 ? 0.0 : oldComponents[2] - value; 91 | newComponents[3] = oldComponents[3]; 92 | } 93 | 94 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 95 | CGColorRef newColor = CGColorCreate(colorSpace, newComponents); 96 | CGColorSpaceRelease(colorSpace); 97 | 98 | UIColor *retColor = [UIColor colorWithCGColor:newColor]; 99 | CGColorRelease(newColor); 100 | 101 | return retColor; 102 | } 103 | 104 | - (BOOL)isLightColor 105 | { 106 | size_t totalComponents = CGColorGetNumberOfComponents(self.CGColor); 107 | BOOL isGreyscale = (totalComponents == 2) ? YES : NO; 108 | 109 | CGFloat *components = (CGFloat *)CGColorGetComponents(self.CGColor); 110 | CGFloat sum; 111 | 112 | if(isGreyscale) { 113 | sum = components[0]; 114 | } 115 | else { 116 | sum = (components[0] + components[1] + components[2]) / 3.0f; 117 | } 118 | 119 | return (sum > 0.8f); 120 | } 121 | 122 | @end -------------------------------------------------------------------------------- /BButton/resources/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/BButton/resources/FontAwesome.ttf -------------------------------------------------------------------------------- /DCChatTableCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1280 5 | 13E28 6 | 3084 7 | 1265.21 8 | 698.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 2083 12 | 13 | 14 | IBProxyObject 15 | IBUIImageView 16 | IBUILabel 17 | IBUITableViewCell 18 | IBUITextView 19 | 20 | 21 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 22 | 23 | 24 | PluginDependencyRecalculationVersion 25 | 26 | 27 | 28 | 29 | IBFilesOwner 30 | IBCocoaTouchFramework 31 | 32 | 33 | IBFirstResponder 34 | IBCocoaTouchFramework 35 | 36 | 37 | 38 | 274 39 | 40 | 41 | 42 | 256 43 | 44 | 45 | 46 | 292 47 | {47, 47} 48 | 49 | 50 | 51 | _NS:9 52 | 53 | 1 54 | MC4yNjQzMDA4MTgxIDAuMjA0NzMwNzc4MiAwLjM4ODU3MjIyNTgAA 55 | 56 | NO 57 | IBCocoaTouchFramework 58 | 59 | NSImage 60 | Icon.png 61 | 62 | 63 | 64 | 65 | 274 66 | {{47, 16}, {273, 1463}} 67 | 68 | 69 | 70 | _NS:9 71 | NO 72 | NO 73 | YES 74 | 75 | 76 | 77 | IBCocoaTouchFramework 78 | NO 79 | NO 80 | NO 81 | NO 82 | NO 83 | NO 84 | NO 85 | Content 86 | 87 | 3 88 | MQA 89 | 90 | 91 | 2 92 | IBCocoaTouchFramework 93 | 94 | 15 95 | 96 | 1 97 | 14 98 | 99 | 100 | Helvetica 101 | 14 102 | 16 103 | 104 | 105 | 106 | 107 | 290 108 | {{55, 4}, {254, 21}} 109 | 110 | 111 | 112 | _NS:9 113 | NO 114 | YES 115 | 7 116 | NO 117 | IBCocoaTouchFramework 118 | Author 119 | 120 | 121 | 3 122 | MC4zMzMzMzMzMzMzAA 123 | 124 | 125 | 1 126 | MCAwIDAAA 127 | 128 | 0 129 | 130 | 2 131 | 15 132 | 133 | 134 | Helvetica-Bold 135 | 15 136 | 16 137 | 138 | NO 139 | 140 | 141 | {320, 47} 142 | 143 | 144 | 145 | _NS:11 146 | 147 | 3 148 | MCAwAA 149 | 150 | NO 151 | YES 152 | 4 153 | YES 154 | IBCocoaTouchFramework 155 | 156 | 157 | {320, 48} 158 | 159 | 160 | 161 | _NS:9 162 | 163 | 1 164 | MC4xMjI4NDc1NzY1IDAuMTIyODQ3NTc2NSAwLjEyMjg0NzU3NjUAA 165 | 166 | NO 167 | 1 168 | IBCocoaTouchFramework 169 | 0 170 | 171 | Message Cell 172 | 173 | 174 | 175 | 176 | 177 | 178 | authorLabel 179 | 180 | 181 | 182 | 93 183 | 184 | 185 | 186 | profileImage 187 | 188 | 189 | 190 | 100 191 | 192 | 193 | 194 | contentTextView 195 | 196 | 197 | 198 | 146 199 | 200 | 201 | 202 | delegate 203 | 204 | 205 | 206 | 137 207 | 208 | 209 | 210 | 211 | 212 | 0 213 | 214 | 215 | 216 | 217 | 218 | -1 219 | 220 | 221 | File's Owner 222 | 223 | 224 | -2 225 | 226 | 227 | 228 | 229 | 37 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 38 240 | 241 | 242 | 243 | 244 | 245 | 99 246 | 247 | 248 | 249 | 250 | 251 | 102 252 | 253 | 254 | 255 | 256 | 257 | 258 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 259 | UIResponder 260 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 261 | 262 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 263 | DCChatTableCell 264 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 265 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 266 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 267 | 268 | 269 | 270 | 271 | 272 | 146 273 | 274 | 275 | 276 | 277 | DCChatTableCell 278 | UITableViewCell 279 | 280 | UILabel 281 | UITextView 282 | UIImageView 283 | 284 | 285 | 286 | authorLabel 287 | UILabel 288 | 289 | 290 | contentTextView 291 | UITextView 292 | 293 | 294 | profileImage 295 | UIImageView 296 | 297 | 298 | 299 | IBProjectSource 300 | ./Classes/DCChatTableCell.h 301 | 302 | 303 | 304 | 305 | 0 306 | IBCocoaTouchFramework 307 | 308 | com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS 309 | 310 | 311 | 312 | com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS 313 | 314 | 315 | YES 316 | 3 317 | 318 | Icon.png 319 | {57, 57} 320 | 321 | 2083 322 | 323 | 324 | -------------------------------------------------------------------------------- /Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/Default-568h@2x.png -------------------------------------------------------------------------------- /Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/Default.png -------------------------------------------------------------------------------- /Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/Default@2x.png -------------------------------------------------------------------------------- /Discord Classic.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Discord Classic.xcodeproj/project.xcworkspace/xcuserdata/trevir.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/Discord Classic.xcodeproj/project.xcworkspace/xcuserdata/trevir.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Discord Classic.xcodeproj/project.xcworkspace/xcuserdata/trevir.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 12 | 13 | IssueFilterStyle 14 | ShowActiveSchemeOnly 15 | LiveSourceIssuesEnabled 16 | 17 | SnapshotAutomaticallyBeforeSignificantChanges 18 | 19 | SnapshotLocationStyle 20 | Default 21 | 22 | 23 | -------------------------------------------------------------------------------- /Discord Classic.xcodeproj/trevir.pbxuser: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | 0975DF96204B5A1B000C4250 /* Discord Classic */ = { 4 | isa = PBXExecutable; 5 | activeArgIndices = ( 6 | ); 7 | argumentStrings = ( 8 | ); 9 | autoAttachOnCrash = 1; 10 | breakpointsEnabled = 0; 11 | configStateDict = { 12 | }; 13 | customDataFormattersEnabled = 1; 14 | dataTipCustomDataFormattersEnabled = 1; 15 | dataTipShowTypeColumn = 1; 16 | dataTipSortType = 0; 17 | debuggerPlugin = GDBDebugging; 18 | disassemblyDisplayState = 0; 19 | enableDebugStr = 1; 20 | environmentEntries = ( 21 | ); 22 | executableSystemSymbolLevel = 0; 23 | executableUserSymbolLevel = 0; 24 | libgmallocEnabled = 0; 25 | name = "Discord Classic"; 26 | showTypeColumn = 0; 27 | sourceDirectories = ( 28 | ); 29 | }; 30 | 0975DFA7204B5A22000C4250 /* Source Control */ = { 31 | isa = PBXSourceControlManager; 32 | fallbackIsa = XCSourceControlManager; 33 | isSCMEnabled = 0; 34 | scmConfiguration = { 35 | repositoryNamesForRoots = { 36 | "" = ""; 37 | }; 38 | }; 39 | }; 40 | 0975DFA8204B5A22000C4250 /* Code sense */ = { 41 | isa = PBXCodeSenseManager; 42 | indexTemplatePath = ""; 43 | }; 44 | 0975DFCA204B5B06000C4250 /* PBXTextBookmark */ = { 45 | isa = PBXTextBookmark; 46 | fRef = 09C6B988204A17AF007EAC26 /* WSWebSocket.m */; 47 | name = "WSWebSocket.m: 25"; 48 | rLen = 0; 49 | rLoc = 1206; 50 | rType = 0; 51 | vrLen = 580; 52 | vrLoc = 0; 53 | }; 54 | 09B57A0C204B5B4E002BB341 /* PBXTextBookmark */ = { 55 | isa = PBXTextBookmark; 56 | fRef = 09C6B988204A17AF007EAC26 /* WSWebSocket.m */; 57 | name = "WSWebSocket.m: 25"; 58 | rLen = 0; 59 | rLoc = 1206; 60 | rType = 0; 61 | vrLen = 1452; 62 | vrLoc = 3; 63 | }; 64 | 09C6B917204A0F54007EAC26 /* Project object */ = { 65 | activeArchitecturePreference = i386; 66 | activeBuildConfigurationName = Debug; 67 | activeExecutable = 0975DF96204B5A1B000C4250 /* Discord Classic */; 68 | activeTarget = 09C6B91F204A0F54007EAC26 /* Discord Classic */; 69 | codeSenseManager = 0975DFA8204B5A22000C4250 /* Code sense */; 70 | executables = ( 71 | 0975DF96204B5A1B000C4250 /* Discord Classic */, 72 | ); 73 | perUserDictionary = { 74 | PBXConfiguration.PBXFileTableDataSource3.PBXFileTableDataSource = { 75 | PBXFileTableDataSourceColumnSortingDirectionKey = "-1"; 76 | PBXFileTableDataSourceColumnSortingKey = PBXFileDataSource_Filename_ColumnID; 77 | PBXFileTableDataSourceColumnWidthsKey = ( 78 | 20, 79 | 356, 80 | 20, 81 | 48, 82 | 43, 83 | 43, 84 | 20, 85 | ); 86 | PBXFileTableDataSourceColumnsKey = ( 87 | PBXFileDataSource_FiletypeID, 88 | PBXFileDataSource_Filename_ColumnID, 89 | PBXFileDataSource_Built_ColumnID, 90 | PBXFileDataSource_ObjectSize_ColumnID, 91 | PBXFileDataSource_Errors_ColumnID, 92 | PBXFileDataSource_Warnings_ColumnID, 93 | PBXFileDataSource_Target_ColumnID, 94 | ); 95 | }; 96 | PBXConfiguration.PBXTargetDataSource.PBXTargetDataSource = { 97 | PBXFileTableDataSourceColumnSortingDirectionKey = "-1"; 98 | PBXFileTableDataSourceColumnSortingKey = PBXFileDataSource_Filename_ColumnID; 99 | PBXFileTableDataSourceColumnWidthsKey = ( 100 | 20, 101 | 316, 102 | 60, 103 | 20, 104 | 48.16259765625, 105 | 43, 106 | 43, 107 | ); 108 | PBXFileTableDataSourceColumnsKey = ( 109 | PBXFileDataSource_FiletypeID, 110 | PBXFileDataSource_Filename_ColumnID, 111 | PBXTargetDataSource_PrimaryAttribute, 112 | PBXFileDataSource_Built_ColumnID, 113 | PBXFileDataSource_ObjectSize_ColumnID, 114 | PBXFileDataSource_Errors_ColumnID, 115 | PBXFileDataSource_Warnings_ColumnID, 116 | ); 117 | }; 118 | PBXPerProjectTemplateStateSaveDate = 541809437; 119 | PBXWorkspaceStateSaveDate = 541809437; 120 | }; 121 | perUserProjectItems = { 122 | 0975DFCA204B5B06000C4250 /* PBXTextBookmark */ = 0975DFCA204B5B06000C4250 /* PBXTextBookmark */; 123 | 09B57A0C204B5B4E002BB341 /* PBXTextBookmark */ = 09B57A0C204B5B4E002BB341 /* PBXTextBookmark */; 124 | }; 125 | sourceControlManager = 0975DFA7204B5A22000C4250 /* Source Control */; 126 | userBuildSettings = { 127 | }; 128 | }; 129 | 09C6B91F204A0F54007EAC26 /* Discord Classic */ = { 130 | activeExec = 0; 131 | executables = ( 132 | 0975DF96204B5A1B000C4250 /* Discord Classic */, 133 | ); 134 | }; 135 | 09C6B943204A0F55007EAC26 /* Discord ClassicTests */ = { 136 | activeExec = 0; 137 | }; 138 | 09C6B988204A17AF007EAC26 /* WSWebSocket.m */ = { 139 | uiCtxt = { 140 | sepNavIntBoundsRect = "{{0, 0}, {614, 9646}}"; 141 | sepNavSelRange = "{1206, 0}"; 142 | sepNavVisRange = "{3, 1452}"; 143 | }; 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /Discord Classic.xcodeproj/xcuserdata/Trevir.xcuserdatad/xcschemes/Discord.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Discord Classic.xcodeproj/xcuserdata/Trevir.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Discord.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 09C6B91F204A0F54007EAC26 16 | 17 | primary 18 | 19 | 20 | 09C6B943204A0F55007EAC26 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Discord Classic/DCAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCAppDelegate.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/2/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Discord Classic/DCAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCAppDelegate.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/2/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCAppDelegate.h" 10 | #import "DCServerCommunicator.h" 11 | 12 | @interface DCAppDelegate() 13 | @property bool shouldReload; 14 | @end 15 | 16 | @implementation DCAppDelegate 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOption{ 19 | self.window.backgroundColor = [UIColor clearColor]; 20 | self.window.opaque = NO; 21 | 22 | self.shouldReload = false; 23 | 24 | [UINavigationBar.appearance setBackgroundImage:[UIImage imageNamed:@"UINavigationBarTexture"] forBarMetrics:UIBarMetricsDefault]; 25 | 26 | if(DCServerCommunicator.sharedInstance.token.length) 27 | [DCServerCommunicator.sharedInstance startCommunicator]; 28 | 29 | return YES; 30 | } 31 | 32 | 33 | - (void)applicationWillResignActive:(UIApplication *)application{ 34 | NSLog(@"Will resign active"); 35 | } 36 | 37 | 38 | - (void)applicationDidEnterBackground:(UIApplication *)application{ 39 | NSLog(@"Did enter background"); 40 | self.shouldReload = DCServerCommunicator.sharedInstance.didAuthenticate; 41 | } 42 | 43 | 44 | - (void)applicationWillEnterForeground:(UIApplication *)application{ 45 | NSLog(@"Will enter foreground"); 46 | } 47 | 48 | 49 | - (void)applicationDidBecomeActive:(UIApplication *)application{ 50 | NSLog(@"Did become active"); 51 | if(self.shouldReload){ 52 | [DCServerCommunicator.sharedInstance sendResume]; 53 | } 54 | } 55 | 56 | 57 | - (void)applicationWillTerminate:(UIApplication *)application{ 58 | NSLog(@"Will terminate"); 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /Discord Classic/DCChannel.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCChannel.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/12/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | /*DCChannel is a representation of a Discord API Channel object. 10 | Its easier to work with than raw JSON data and has some handy 11 | built in functions*/ 12 | 13 | #import 14 | #import "DCGuild.h" 15 | #import "DCMessage.h" 16 | 17 | @interface DCChannel : NSObject 18 | @property NSString* snowflake; 19 | @property NSString* name; 20 | @property NSString* lastMessageId; 21 | @property NSString* lastReadMessageId; 22 | @property bool unread; 23 | @property bool muted; 24 | @property int type; 25 | @property DCGuild* parentGuild; 26 | 27 | -(void)checkIfRead; 28 | - (void)sendMessage:(NSString*)message; 29 | - (void)ackMessage:(NSString*)message; 30 | - (void)sendImage:(UIImage*)image; 31 | - (NSArray*)getMessages:(int)numberOfMessages beforeMessage:(DCMessage*)message; 32 | @end 33 | -------------------------------------------------------------------------------- /Discord Classic/DCChannel.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCChannel.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/12/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCChannel.h" 10 | #import "DCServerCommunicator.h" 11 | #import "DCTools.h" 12 | 13 | @interface DCChannel() 14 | 15 | @property NSURLConnection *connection; 16 | 17 | @end 18 | 19 | @implementation DCChannel 20 | 21 | -(NSString *)description{ 22 | return [NSString stringWithFormat:@"[Channel] Snowflake: %@, Type: %i, Read: %d, Name: %@", self.snowflake, self.type, self.unread, self.name]; 23 | } 24 | 25 | -(void)checkIfRead{ 26 | self.unread = (!self.muted && self.lastReadMessageId != (id)NSNull.null && ![self.lastReadMessageId isEqualToString:self.lastMessageId]); 27 | [self.parentGuild checkIfRead]; 28 | } 29 | 30 | - (void)sendMessage:(NSString*)message { 31 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 32 | NSURL* channelURL = [NSURL URLWithString: [NSString stringWithFormat:@"https://discordapp.com/api/channels/%@/messages", self.snowflake]]; 33 | 34 | NSMutableURLRequest *urlRequest=[NSMutableURLRequest requestWithURL:channelURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1]; 35 | 36 | NSString* messageString = [NSString stringWithFormat:@"{\"content\":\"%@\"}", message]; 37 | 38 | [urlRequest setHTTPMethod:@"POST"]; 39 | 40 | [urlRequest setHTTPBody:[NSData dataWithBytes:[messageString UTF8String] length:[messageString length]]]; 41 | [urlRequest addValue:DCServerCommunicator.sharedInstance.token forHTTPHeaderField:@"Authorization"]; 42 | [urlRequest addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 43 | 44 | 45 | NSError *error = nil; 46 | NSHTTPURLResponse *responseCode = nil; 47 | 48 | [DCTools checkData:[NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&responseCode error:&error] withError:error]; 49 | }); 50 | } 51 | 52 | 53 | 54 | - (void)sendImage:(UIImage*)image { 55 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 56 | NSURL* channelURL = [NSURL URLWithString: [NSString stringWithFormat:@"https://discordapp.com/api/channels/%@/messages", self.snowflake]]; 57 | 58 | NSMutableURLRequest *urlRequest=[NSMutableURLRequest requestWithURL:channelURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10]; 59 | 60 | [urlRequest setHTTPMethod:@"POST"]; 61 | 62 | NSString *boundary = @"---------------------------14737809831466499882746641449"; 63 | 64 | NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; 65 | [urlRequest addValue:contentType forHTTPHeaderField: @"Content-Type"]; 66 | [urlRequest addValue:DCServerCommunicator.sharedInstance.token forHTTPHeaderField:@"Authorization"]; 67 | 68 | NSMutableData *postbody = NSMutableData.new; 69 | [postbody appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 70 | [postbody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"upload.jpg\"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; 71 | [postbody appendData:[@"Content-Type: image/jpeg\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; 72 | [postbody appendData:[NSData dataWithData:UIImageJPEGRepresentation(image, 0.9f)]]; 73 | [postbody appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 74 | [postbody appendData:[@"Content-Disposition: form-data; name=\"content\"\r\n\r\n " dataUsingEncoding:NSUTF8StringEncoding]]; 75 | [postbody appendData:[[NSString stringWithFormat:@"\r\n--%@--", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 76 | 77 | [urlRequest setHTTPBody:postbody]; 78 | 79 | 80 | NSError *error = nil; 81 | NSHTTPURLResponse *responseCode = nil; 82 | 83 | [DCTools checkData:[NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&responseCode error:&error] withError:error]; 84 | }); 85 | } 86 | 87 | 88 | 89 | - (void)ackMessage:(NSString*)messageId{ 90 | self.lastReadMessageId = messageId; 91 | 92 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 93 | 94 | NSURL* channelURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://discordapp.com/api/channels/%@/messages/%@/ack", self.snowflake, messageId]]; 95 | 96 | NSMutableURLRequest *urlRequest=[NSMutableURLRequest requestWithURL:channelURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1]; 97 | 98 | [urlRequest setHTTPMethod:@"POST"]; 99 | 100 | [urlRequest addValue:DCServerCommunicator.sharedInstance.token forHTTPHeaderField:@"Authorization"]; 101 | [urlRequest addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 102 | 103 | 104 | NSError *error = nil; 105 | NSHTTPURLResponse *responseCode = nil; 106 | 107 | [DCTools checkData:[NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&responseCode error:&error] withError:error]; 108 | }); 109 | } 110 | 111 | 112 | 113 | - (NSArray*)getMessages:(int)numberOfMessages beforeMessage:(DCMessage*)message{ 114 | 115 | NSMutableArray* messages = NSMutableArray.new; 116 | 117 | //Generate URL from args 118 | NSMutableString* getChannelAddress = [[NSString stringWithFormat: @"https://discordapp.com/api/channels/%@/messages?", self.snowflake] mutableCopy]; 119 | 120 | if(numberOfMessages) 121 | [getChannelAddress appendString:[NSString stringWithFormat:@"limit=%i", numberOfMessages]]; 122 | if(numberOfMessages && message) 123 | [getChannelAddress appendString:@"&"]; 124 | if(message) 125 | [getChannelAddress appendString:[NSString stringWithFormat:@"before=%@", message.snowflake]]; 126 | 127 | NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:getChannelAddress] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:2]; 128 | 129 | [urlRequest addValue:DCServerCommunicator.sharedInstance.token forHTTPHeaderField:@"Authorization"]; 130 | [urlRequest addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 131 | 132 | NSError *error; 133 | NSHTTPURLResponse *responseCode; 134 | NSData *response = [DCTools checkData:[NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&responseCode error:&error] withError:error]; 135 | 136 | if(response){ 137 | NSArray* parsedResponse = [NSJSONSerialization JSONObjectWithData:response options:0 error:&error]; 138 | 139 | if(parsedResponse.count > 0) 140 | for(NSDictionary* jsonMessage in parsedResponse) 141 | [messages insertObject:[DCTools convertJsonMessage:jsonMessage] atIndex:0]; 142 | 143 | if(messages.count > 0) 144 | return messages; 145 | 146 | [DCTools alert:@"No messages!" withMessage:@"No further messages could be found"]; 147 | } 148 | 149 | return nil; 150 | } 151 | 152 | 153 | @end 154 | -------------------------------------------------------------------------------- /Discord Classic/DCChannelListViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCChannelViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/5/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DCGuild.h" 11 | 12 | @interface DCChannelListViewController : UITableViewController 13 | @property DCGuild* selectedGuild; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Discord Classic/DCChannelListViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCChannelViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/5/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCChannelListViewController.h" 10 | #import "DCChatViewController.h" 11 | #import "DCServerCommunicator.h" 12 | #import "DCGuild.h" 13 | #import "DCChannel.h" 14 | #import "TRMalleableFrameView.h" 15 | 16 | @interface DCChannelListViewController () 17 | @property int selectedChannelIndex; 18 | @property DCChannel* selectedChannel; 19 | @end 20 | 21 | @implementation DCChannelListViewController 22 | 23 | - (void)viewDidLoad{ 24 | [super viewDidLoad]; 25 | 26 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleMessageAck) name:@"MESSAGE ACK" object:nil]; 27 | } 28 | 29 | 30 | -(void)viewWillAppear:(BOOL)animated{ 31 | [self.navigationItem setTitle:self.selectedGuild.name]; 32 | [DCServerCommunicator.sharedInstance setSelectedChannel:nil]; 33 | } 34 | 35 | 36 | - (void)handleMessageAck { 37 | [self.tableView reloadData]; 38 | } 39 | 40 | 41 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 42 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Channel Cell"]; 43 | 44 | //Show blue indicator if channel contains any unread messages 45 | DCChannel* channelAtRowIndex = [self.selectedGuild.channels objectAtIndex:indexPath.row]; 46 | if(channelAtRowIndex.unread) 47 | [cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton]; 48 | else 49 | [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; 50 | 51 | //Channel name 52 | [cell.textLabel setText:channelAtRowIndex.name]; 53 | 54 | return cell; 55 | } 56 | 57 | 58 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 59 | DCServerCommunicator.sharedInstance.selectedChannel = [self.selectedGuild.channels objectAtIndex:indexPath.row]; 60 | 61 | //Mark channel messages as read and refresh the channel object accordingly 62 | [DCServerCommunicator.sharedInstance.selectedChannel ackMessage:DCServerCommunicator.sharedInstance.selectedChannel.lastMessageId]; 63 | [DCServerCommunicator.sharedInstance.selectedChannel checkIfRead]; 64 | 65 | //Remove the blue indicator since the channel has been read 66 | [[self.tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; 67 | 68 | //Transition to chat view 69 | [self performSegueWithIdentifier:@"Channels to Chat" sender:self]; 70 | } 71 | 72 | 73 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ 74 | if ([segue.identifier isEqualToString:@"Channels to Chat"]){ 75 | DCChatViewController *chatViewController = [segue destinationViewController]; 76 | 77 | if ([chatViewController isKindOfClass:DCChatViewController.class]){ 78 | 79 | //Initialize messages 80 | chatViewController.messages = NSMutableArray.new; 81 | 82 | //Add a '#' if appropriate to the chanel name in the navigation bar 83 | NSString* formattedChannelName; 84 | if(DCServerCommunicator.sharedInstance.selectedChannel.type == 0) 85 | formattedChannelName = [@"#" stringByAppendingString:DCServerCommunicator.sharedInstance.selectedChannel.name]; 86 | else 87 | formattedChannelName = DCServerCommunicator.sharedInstance.selectedChannel.name; 88 | [chatViewController.navigationItem setTitle:formattedChannelName]; 89 | 90 | //Populate the message view with the last 50 messages 91 | [chatViewController getMessages:50 beforeMessage:nil]; 92 | 93 | //Chat view is watching the present conversation (auto scroll with new messages) 94 | [chatViewController setViewingPresentTime:true]; 95 | } 96 | } 97 | } 98 | 99 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{return 1;} 100 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{return self.selectedGuild.channels.count;} 101 | @end 102 | -------------------------------------------------------------------------------- /Discord Classic/DCChatTableCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCChatTableCell.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 4/7/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCChatTableCell : UITableViewCell 12 | @property (strong, nonatomic) IBOutlet UILabel *authorLabel; 13 | @property (weak, nonatomic) IBOutlet UIImageView *profileImage; 14 | @property (strong, nonatomic) IBOutlet UITextView *contentTextView; 15 | @end 16 | -------------------------------------------------------------------------------- /Discord Classic/DCChatTableCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCChatTableCell.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 4/7/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCChatTableCell.h" 10 | 11 | @implementation DCChatTableCell 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Discord Classic/DCChatViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCChatViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/6/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DCChannel.h" 11 | #import "DCMessage.h" 12 | 13 | @interface DCChatViewController : UIViewController 14 | - (void)getMessages:(int)numberOfMessages beforeMessage:(DCMessage*)message; 15 | 16 | @property (weak, nonatomic) IBOutlet UIToolbar *toolbar; 17 | @property (weak, nonatomic) IBOutlet UITableView *chatTableView; 18 | @property (weak, nonatomic) IBOutlet UITextField *inputField; 19 | @property bool viewingPresentTime; 20 | 21 | @property DCMessage *selectedMessage; 22 | 23 | @property NSMutableArray* messages; 24 | @end 25 | -------------------------------------------------------------------------------- /Discord Classic/DCChatViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCChatViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/6/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCChatViewController.h" 10 | #import "DCServerCommunicator.h" 11 | #import "TRMalleableFrameView.h" 12 | #import "DCMessage.h" 13 | #import "DCTools.h" 14 | #import "DCChatTableCell.h" 15 | #import "DCUser.h" 16 | #import "DCImageViewController.h" 17 | #import "TRMalleableFrameView.h" 18 | 19 | @interface DCChatViewController() 20 | @property int numberOfMessagesLoaded; 21 | @property UIImage* selectedImage; 22 | @property UIRefreshControl *refreshControl; 23 | @end 24 | 25 | @implementation DCChatViewController 26 | 27 | - (void)viewDidLoad{ 28 | [super viewDidLoad]; 29 | 30 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleMessageCreate:) name:@"MESSAGE CREATE" object:nil]; 31 | 32 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleMessageDelete:) name:@"MESSAGE DELETE" object:nil]; 33 | 34 | [NSNotificationCenter.defaultCenter addObserver:self.chatTableView selector:@selector(reloadData) name:@"RELOAD CHAT DATA" object:nil]; 35 | 36 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleReady) name:@"READY" object:nil]; 37 | 38 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; 39 | 40 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; 41 | 42 | 43 | self.refreshControl = UIRefreshControl.new; 44 | self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"Earlier messages"]; 45 | 46 | [self.chatTableView addSubview:self.refreshControl]; 47 | 48 | [self.refreshControl addTarget:self action:@selector(get50MoreMessages:) forControlEvents:UIControlEventValueChanged]; 49 | } 50 | 51 | 52 | - (void)handleReady { 53 | 54 | if(DCServerCommunicator.sharedInstance.selectedChannel){ 55 | self.messages = NSMutableArray.new; 56 | 57 | [self getMessages:50 beforeMessage:nil]; 58 | } 59 | 60 | [self.refreshControl endRefreshing]; 61 | } 62 | 63 | 64 | - (void)handleMessageCreate:(NSNotification*)notification { 65 | DCMessage* newMessage = [DCTools convertJsonMessage:notification.userInfo]; 66 | 67 | [self.messages addObject:newMessage]; 68 | [self.chatTableView reloadData]; 69 | 70 | if(self.viewingPresentTime) 71 | [self.chatTableView setContentOffset:CGPointMake(0, self.chatTableView.contentSize.height - self.chatTableView.frame.size.height) animated:NO]; 72 | } 73 | 74 | 75 | - (void)handleMessageDelete:(NSNotification*)notification { 76 | DCMessage *compareMessage = DCMessage.new; 77 | compareMessage.snowflake = [notification.userInfo valueForKey:@"id"]; 78 | 79 | [self.messages removeObject:compareMessage]; 80 | [self.chatTableView reloadData]; 81 | 82 | } 83 | 84 | 85 | - (void)getMessages:(int)numberOfMessages beforeMessage:(DCMessage*)message{ 86 | NSArray* newMessages = [DCServerCommunicator.sharedInstance.selectedChannel getMessages:numberOfMessages beforeMessage:message]; 87 | 88 | if(newMessages){ 89 | NSRange range = NSMakeRange(0, [newMessages count]); 90 | NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:range]; 91 | [self.messages insertObjects:newMessages atIndexes:indexSet]; 92 | 93 | [self.chatTableView reloadData]; 94 | 95 | dispatch_async(dispatch_get_main_queue(), ^{ 96 | int scrollOffset = -self.chatTableView.height; 97 | for(DCMessage* newMessage in newMessages) 98 | scrollOffset += newMessage.contentHeight + newMessage.embeddedImageCount * 220; 99 | 100 | [self.chatTableView setContentOffset:CGPointMake(0, scrollOffset) animated:NO]; 101 | }); 102 | } 103 | 104 | [self.refreshControl endRefreshing]; 105 | } 106 | 107 | 108 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 109 | //static NSString *guildCellIdentifier = @"Channel Cell"; 110 | 111 | [tableView registerNib:[UINib nibWithNibName:@"DCChatTableCell" bundle:nil] forCellReuseIdentifier:@"Message Cell"]; 112 | DCChatTableCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Message Cell"]; 113 | 114 | DCMessage* messageAtRowIndex = [self.messages objectAtIndex:indexPath.row]; 115 | 116 | [cell.authorLabel setText:messageAtRowIndex.author.username]; 117 | 118 | [cell.contentTextView setText:messageAtRowIndex.content]; 119 | 120 | [cell.contentTextView setHeight:[cell.contentTextView sizeThatFits:CGSizeMake(cell.contentTextView.width, MAXFLOAT)].height]; 121 | 122 | [cell.profileImage setImage:messageAtRowIndex.author.profileImage]; 123 | 124 | [cell.contentView setBackgroundColor:messageAtRowIndex.pingingUser? [UIColor redColor] : [UIColor clearColor]]; 125 | 126 | for (UIView *subView in cell.subviews) { 127 | if ([subView isKindOfClass:[UIImageView class]]) { 128 | [subView removeFromSuperview]; 129 | } 130 | } 131 | 132 | int imageViewOffset = cell.contentTextView.height + 37; 133 | 134 | for(UIImage* image in messageAtRowIndex.embeddedImages){ 135 | UIImageView* imageView = UIImageView.new; 136 | [imageView setFrame:CGRectMake(11, imageViewOffset, self.chatTableView.width - 22, 200)]; 137 | [imageView setImage:image]; 138 | imageViewOffset += 210; 139 | 140 | [imageView setContentMode: UIViewContentModeScaleAspectFit]; 141 | 142 | UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedImage:)]; 143 | singleTap.numberOfTapsRequired = 1; 144 | imageView.userInteractionEnabled = YES; 145 | [imageView addGestureRecognizer:singleTap]; 146 | 147 | [cell addSubview:imageView]; 148 | } 149 | return cell; 150 | } 151 | 152 | 153 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ 154 | DCMessage* messageAtRowIndex = [self.messages objectAtIndex:indexPath.row]; 155 | 156 | return messageAtRowIndex.contentHeight + messageAtRowIndex.embeddedImageCount * 220; 157 | } 158 | 159 | 160 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 161 | self.selectedMessage = self.messages[indexPath.row]; 162 | 163 | if([self.selectedMessage.author.snowflake isEqualToString: DCServerCommunicator.sharedInstance.snowflake]){ 164 | UIActionSheet *messageActionSheet = [[UIActionSheet alloc] initWithTitle:self.selectedMessage.content delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete" otherButtonTitles:nil]; 165 | [messageActionSheet setTag:1]; 166 | [messageActionSheet setDelegate:self]; 167 | [messageActionSheet showInView:self.view]; 168 | } 169 | } 170 | 171 | 172 | - (void)actionSheet:(UIActionSheet *)popup clickedButtonAtIndex:(NSInteger)buttonIndex { 173 | if(buttonIndex == 0) 174 | [self.selectedMessage deleteMessage]; 175 | } 176 | 177 | 178 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView{ 179 | self.viewingPresentTime = (scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.height - 10); 180 | } 181 | 182 | -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{return 1;} 183 | 184 | -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{return self.messages.count;} 185 | 186 | 187 | - (void)keyboardWillShow:(NSNotification *)notification { 188 | 189 | //thx to Pierre Legrain 190 | //http://pyl.io/2015/08/17/animating-in-sync-with-ios-keyboard/ 191 | 192 | int keyboardHeight = [[notification.userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height; 193 | float keyboardAnimationDuration = [[notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]; 194 | int keyboardAnimationCurve = [[notification.userInfo objectForKey: UIKeyboardAnimationCurveUserInfoKey] integerValue]; 195 | 196 | [UIView beginAnimations:nil context:NULL]; 197 | [UIView setAnimationDuration:keyboardAnimationDuration]; 198 | [UIView setAnimationCurve:keyboardAnimationCurve]; 199 | [UIView setAnimationBeginsFromCurrentState:YES]; 200 | [self.chatTableView setHeight:self.view.height - keyboardHeight - self.toolbar.height]; 201 | [self.toolbar setY:self.view.height - keyboardHeight - self.toolbar.height]; 202 | [UIView commitAnimations]; 203 | 204 | 205 | if(self.viewingPresentTime) 206 | [self.chatTableView setContentOffset:CGPointMake(0, self.chatTableView.contentSize.height - self.chatTableView.frame.size.height) animated:NO]; 207 | } 208 | 209 | 210 | - (void)keyboardWillHide:(NSNotification *)notification { 211 | 212 | float keyboardAnimationDuration = [[notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]; 213 | int keyboardAnimationCurve = [[notification.userInfo objectForKey: UIKeyboardAnimationCurveUserInfoKey] integerValue]; 214 | 215 | [UIView beginAnimations:nil context:NULL]; 216 | [UIView setAnimationDuration:keyboardAnimationDuration]; 217 | [UIView setAnimationCurve:keyboardAnimationCurve]; 218 | [UIView setAnimationBeginsFromCurrentState:YES]; 219 | [self.chatTableView setHeight:self.view.height - self.toolbar.height]; 220 | [self.toolbar setY:self.view.height - self.toolbar.height]; 221 | [UIView commitAnimations]; 222 | } 223 | 224 | - (IBAction)sendMessage:(id)sender { 225 | if(![self.inputField.text isEqual: @""]){ 226 | [DCServerCommunicator.sharedInstance.selectedChannel sendMessage:self.inputField.text]; 227 | [self.inputField setText:@""]; 228 | }else 229 | [self.inputField resignFirstResponder]; 230 | 231 | if(self.viewingPresentTime) 232 | [self.chatTableView setContentOffset:CGPointMake(0, self.chatTableView.contentSize.height - self.chatTableView.frame.size.height) animated:YES]; 233 | } 234 | 235 | - (void)tappedImage:(UITapGestureRecognizer *)sender { 236 | [self.inputField resignFirstResponder]; 237 | self.selectedImage = ((UIImageView*)sender.view).image; 238 | [self performSegueWithIdentifier:@"Chat to Gallery" sender:self]; 239 | } 240 | 241 | -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ 242 | if ([segue.identifier isEqualToString:@"Chat to Gallery"]){ 243 | 244 | DCImageViewController *imageViewController = [segue destinationViewController]; 245 | 246 | if ([imageViewController isKindOfClass:DCImageViewController.class]){ 247 | dispatch_async(dispatch_get_main_queue(), ^{ 248 | [imageViewController.imageView setImage:self.selectedImage]; 249 | }); 250 | } 251 | } 252 | } 253 | 254 | 255 | - (IBAction)chooseImage:(id)sender { 256 | 257 | [self.inputField resignFirstResponder]; 258 | 259 | UIImagePickerController *picker = UIImagePickerController.new; 260 | 261 | picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; 262 | 263 | [picker setDelegate:self]; 264 | 265 | [self presentModalViewController:picker animated:YES]; 266 | } 267 | 268 | 269 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { 270 | 271 | [picker dismissModalViewControllerAnimated:YES]; 272 | 273 | UIImage* originalImage = [info objectForKey:UIImagePickerControllerEditedImage]; 274 | 275 | if(originalImage==nil) 276 | originalImage = [info objectForKey:UIImagePickerControllerOriginalImage]; 277 | 278 | if(originalImage==nil) 279 | originalImage = [info objectForKey:UIImagePickerControllerCropRect]; 280 | 281 | [DCServerCommunicator.sharedInstance.selectedChannel sendImage:originalImage]; 282 | } 283 | 284 | 285 | -(void)get50MoreMessages:(UIRefreshControl *)control {[self getMessages:50 beforeMessage:[self.messages objectAtIndex:0]];} 286 | @end -------------------------------------------------------------------------------- /Discord Classic/DCGuild.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCGuild.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/12/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | /*DCGuild is a representation of a Discord API Guild object. 10 | Its easier to work with than raw JSON data and has some handy 11 | built in functions*/ 12 | 13 | #import 14 | 15 | @interface DCGuild : NSObject 16 | //ID/snowflake 17 | @property NSString* snowflake; 18 | //Name 19 | @property NSString* name; 20 | //Icon for the guild 21 | @property UIImage* icon; 22 | //Array of child DCCannel objects 23 | @property NSMutableArray* channels; 24 | //Whether or not the guild has any unread child channels 25 | @property bool unread; 26 | 27 | @property NSMutableDictionary* members; 28 | 29 | -(void)checkIfRead; 30 | @end 31 | -------------------------------------------------------------------------------- /Discord Classic/DCGuild.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCGuild.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/12/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCGuild.h" 10 | #import "DCChannel.h" 11 | 12 | @implementation DCGuild 13 | 14 | -(NSString *)description{ 15 | return [NSString stringWithFormat:@"[Guild] Snowflake: %@, Read: %d, Name: %@, Channels: %@", self.snowflake, self.unread, self.name, self.channels]; 16 | } 17 | 18 | -(void)checkIfRead{ 19 | /*Loop through all child channels 20 | if any single one is unread, the guild 21 | as a whole is unread*/ 22 | for(DCChannel* channel in self.channels){ 23 | if(channel.unread){ 24 | self.unread = true; 25 | return; 26 | } 27 | } 28 | 29 | [self setUnread:false]; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Discord Classic/DCGuildListViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCGuildViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/4/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCGuildListViewController : UITableViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Discord Classic/DCGuildListViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCGuildViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/4/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCGuildListViewController.h" 10 | #import "DCChannelListViewController.h" 11 | #import "DCServerCommunicator.h" 12 | #import "DCGuild.h" 13 | #import "DCTools.h" 14 | 15 | @implementation DCGuildListViewController 16 | 17 | - (void)viewDidLoad{ 18 | [super viewDidLoad]; 19 | 20 | //Go to settings if no token is set 21 | if(!DCServerCommunicator.sharedInstance.token.length) 22 | [self performSegueWithIdentifier:@"to Settings" sender:self]; 23 | 24 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleReady) name:@"READY" object:nil]; 25 | 26 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleReady) name:@"MESSAGE ACK" object:nil]; 27 | 28 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleReady) name:@"RELOAD GUILD LIST" object:nil]; 29 | } 30 | 31 | 32 | - (void)handleReady { 33 | //Refresh tableView data on READY notification 34 | [self.tableView reloadData]; 35 | 36 | if(!self.refreshControl){ 37 | self.refreshControl = UIRefreshControl.new; 38 | self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"Reauthenticate"]; 39 | 40 | [self.tableView addSubview:self.refreshControl]; 41 | 42 | [self.refreshControl addTarget:self action:@selector(reconnect) forControlEvents:UIControlEventValueChanged]; 43 | } 44 | } 45 | 46 | 47 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 48 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Guild Cell"]; 49 | 50 | DCGuild* guildAtRowIndex = [DCServerCommunicator.sharedInstance.guilds objectAtIndex:indexPath.row]; 51 | 52 | //Show blue indicator if guild has any unread messages 53 | if(guildAtRowIndex.unread) 54 | [cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton]; 55 | else 56 | [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; 57 | 58 | //Guild name and icon 59 | [cell.textLabel setText:guildAtRowIndex.name]; 60 | [cell.imageView setImage:guildAtRowIndex.icon]; 61 | 62 | return cell; 63 | } 64 | 65 | 66 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 67 | 68 | if([DCServerCommunicator.sharedInstance.guilds objectAtIndex:indexPath.row] != DCServerCommunicator.sharedInstance.selectedGuild){ 69 | //Clear the loaded users array for lazy memory management. This will be fleshed out more later 70 | DCServerCommunicator.sharedInstance.loadedUsers = NSMutableDictionary.new; 71 | //Assign the selected guild 72 | DCServerCommunicator.sharedInstance.selectedGuild = [DCServerCommunicator.sharedInstance.guilds objectAtIndex:indexPath.row]; 73 | } 74 | 75 | //Transition to channel list 76 | [self performSegueWithIdentifier:@"Guilds to Channels" sender:self]; 77 | } 78 | 79 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ 80 | if ([segue.identifier isEqualToString:@"Guilds to Channels"]){ 81 | 82 | DCChannelListViewController *channelListViewController = [segue destinationViewController]; 83 | 84 | if ([channelListViewController isKindOfClass:DCChannelListViewController.class]) 85 | //Assign selected guild for the channel list we are transitioning to. 86 | channelListViewController.selectedGuild = DCServerCommunicator.sharedInstance.selectedGuild; 87 | } 88 | } 89 | 90 | - (IBAction)joinGuildPrompt:(id)sender{ 91 | UIAlertView *joinPrompt = [[UIAlertView alloc] initWithTitle:@"Enter invite code" 92 | message:nil 93 | delegate:self 94 | cancelButtonTitle:@"Cancel" 95 | otherButtonTitles:@"Join", nil]; 96 | 97 | [joinPrompt setAlertViewStyle:UIAlertViewStylePlainTextInput]; 98 | [joinPrompt setDelegate:self]; 99 | [joinPrompt show]; 100 | } 101 | 102 | - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ 103 | if([[alertView buttonTitleAtIndex:buttonIndex] isEqualToString:@"Join"]) 104 | [DCTools joinGuild:[alertView textFieldAtIndex:0].text]; 105 | } 106 | 107 | - (void)reconnect { 108 | [DCServerCommunicator.sharedInstance reconnect]; 109 | [self.refreshControl endRefreshing]; 110 | } 111 | 112 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{return 1;} 113 | 114 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{return DCServerCommunicator.sharedInstance.guilds.count;} 115 | 116 | @end -------------------------------------------------------------------------------- /Discord Classic/DCImageViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCImageViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/17/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCImageViewController : UIViewController 12 | 13 | @property (weak, nonatomic) IBOutlet UIImageView *imageView; 14 | @property (strong, nonatomic) IBOutlet UIScrollView *scrollView; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Discord Classic/DCImageViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCImageViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/17/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCImageViewController.h" 10 | 11 | @interface DCImageViewController () 12 | 13 | @end 14 | 15 | @implementation DCImageViewController 16 | 17 | - (void)viewDidLoad{ 18 | [super viewDidLoad]; 19 | } 20 | 21 | - (void)viewDidUnload { 22 | [self setImageView:nil]; 23 | [self setScrollView:nil]; 24 | [super viewDidUnload]; 25 | } 26 | 27 | -(IBAction)presentShareSheet:(id)sender{ 28 | //Show share sheet with appropriate options 29 | NSArray *itemsToShare = @[self.imageView.image]; 30 | UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:itemsToShare applicationActivities:nil]; 31 | [self presentViewController:activityVC animated:YES completion:nil]; 32 | } 33 | 34 | -(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{return self.imageView;} 35 | @end 36 | -------------------------------------------------------------------------------- /Discord Classic/DCInfoPageViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCInfoPageViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/24/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCInfoPageViewController : UITableViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Discord Classic/DCInfoPageViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCInfoPageViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/24/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCInfoPageViewController.h" 10 | 11 | @interface DCInfoPageViewController () 12 | @property NSArray *creditLinks; 13 | @end 14 | 15 | @implementation DCInfoPageViewController 16 | 17 | - (void)viewDidLoad{ 18 | [super viewDidLoad]; 19 | self.creditLinks = [NSArray arrayWithObjects:@"https://twitter.com/_cellomonster", @"https://twitter.com/TyBrasher", @"https://twitter.com/IPGSecondary", @"https://github.com/ndcube", @"https://discordapp.com", nil]; 20 | } 21 | 22 | - (void)didReceiveMemoryWarning{[super didReceiveMemoryWarning];} 23 | 24 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 25 | [UIApplication.sharedApplication openURL:[NSURL URLWithString:self.creditLinks[indexPath.row + indexPath.section * 3]]]; 26 | [tableView deselectRowAtIndexPath:indexPath animated:NO]; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Discord Classic/DCMessage.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCMessage.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 4/6/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DCUser.h" 11 | 12 | @interface DCMessage : NSObject 13 | @property NSString* snowflake; 14 | @property DCUser* author; 15 | @property NSString* content; 16 | @property int embeddedImageCount; 17 | @property NSMutableArray* embeddedImages; 18 | @property int contentHeight; 19 | @property bool pingingUser; 20 | 21 | - (void)deleteMessage; 22 | - (BOOL)isEqual:(id)other; 23 | @end 24 | -------------------------------------------------------------------------------- /Discord Classic/DCMessage.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCMessage.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 4/7/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCMessage.h" 10 | #import "DCServerCommunicator.h" 11 | #import "DCTools.h" 12 | 13 | @implementation DCMessage 14 | 15 | - (void)deleteMessage{ 16 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 17 | NSURL* messageURL = [NSURL URLWithString: [NSString stringWithFormat:@"https://discordapp.com/api/v6/channels/%@/messages/%@", DCServerCommunicator.sharedInstance.selectedChannel.snowflake, self.snowflake]]; 18 | 19 | NSMutableURLRequest *urlRequest=[NSMutableURLRequest requestWithURL:messageURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1]; 20 | 21 | [urlRequest setHTTPMethod:@"DELETE"]; 22 | 23 | [urlRequest addValue:DCServerCommunicator.sharedInstance.token forHTTPHeaderField:@"Authorization"]; 24 | [urlRequest addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 25 | 26 | 27 | NSError *error = nil; 28 | NSHTTPURLResponse *responseCode = nil; 29 | 30 | [DCTools checkData:[NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&responseCode error:&error] withError:error]; 31 | }); 32 | } 33 | 34 | - (BOOL)isEqual:(id)other{ 35 | if (!other || ![other isKindOfClass:DCMessage.class]) 36 | return NO; 37 | 38 | return [self.snowflake isEqual:((DCMessage*)other).snowflake]; 39 | } 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /Discord Classic/DCServerCommunicator.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCServerCommunicator.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/4/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "WSWebSocket.h" 11 | #import "DCGuildListViewController.h" 12 | #import "DCChannelListViewController.h" 13 | #import "DCChatViewController.h" 14 | 15 | @interface DCServerCommunicator : NSObject 16 | 17 | @property WSWebSocket* websocket; 18 | @property NSString* token; 19 | @property NSString* gatewayURL; 20 | @property NSMutableDictionary* userChannelSettings; 21 | 22 | @property NSString* snowflake; 23 | 24 | @property NSMutableArray* guilds; 25 | @property NSMutableDictionary* channels; 26 | @property NSMutableDictionary* loadedUsers; 27 | 28 | @property DCGuild* selectedGuild; 29 | @property DCChannel* selectedChannel; 30 | 31 | @property bool didAuthenticate; 32 | 33 | + (DCServerCommunicator *)sharedInstance; 34 | - (void)startCommunicator; 35 | - (void)sendResume; 36 | - (void)reconnect; 37 | - (void)sendHeartbeat:(NSTimer *)timer; 38 | - (void)sendJSON:(NSDictionary*)dictionary; 39 | @end 40 | -------------------------------------------------------------------------------- /Discord Classic/DCServerCommunicator.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCServerCommunicator.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/4/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCServerCommunicator.h" 10 | #import "DCGuild.h" 11 | #import "DCChannel.h" 12 | #import "DCTools.h" 13 | 14 | @interface DCServerCommunicator() 15 | @property bool didRecieveHeartbeatResponse; 16 | @property bool shouldResume; 17 | @property bool heartbeatDefined; 18 | 19 | @property bool identifyCooldown; 20 | 21 | @property int sequenceNumber; 22 | @property NSString* sessionId; 23 | 24 | @property NSTimer* cooldownTimer; 25 | @property UIAlertView* alertView; 26 | @end 27 | 28 | 29 | @implementation DCServerCommunicator 30 | 31 | + (DCServerCommunicator *)sharedInstance { 32 | static DCServerCommunicator *sharedInstance = nil; 33 | 34 | if (sharedInstance == nil) { 35 | //Initialize if a sharedInstance does not yet exist 36 | sharedInstance = DCServerCommunicator.new; 37 | sharedInstance.gatewayURL = @"wss://gateway.discord.gg/?encoding=json&v=6"; 38 | sharedInstance.token = [NSUserDefaults.standardUserDefaults stringForKey:@"token"]; 39 | 40 | 41 | 42 | sharedInstance.alertView = [UIAlertView.alloc initWithTitle:@"Connecting" message:@"\n" delegate:self cancelButtonTitle:nil otherButtonTitles:nil]; 43 | 44 | UIActivityIndicatorView *spinner = [UIActivityIndicatorView.alloc initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; 45 | [spinner setCenter:CGPointMake(139.5, 75.5)]; 46 | 47 | [sharedInstance.alertView addSubview:spinner]; 48 | [spinner startAnimating]; 49 | } 50 | 51 | return sharedInstance; 52 | } 53 | 54 | 55 | - (void)startCommunicator{ 56 | 57 | [self.alertView show]; 58 | 59 | self.didAuthenticate = false; 60 | 61 | if(self.token!=nil){ 62 | 63 | //Establish websocket connection with Discord 64 | NSURL *websocketUrl = [NSURL URLWithString:self.gatewayURL]; 65 | self.websocket = [WSWebSocket.alloc initWithURL:websocketUrl protocols:nil]; 66 | 67 | //To prevent retain cycle 68 | __weak typeof(self) weakSelf = self; 69 | 70 | [self.websocket setTextCallback:^(NSString *responseString) { 71 | 72 | //Parse JSON to a dictionary 73 | NSDictionary *parsedJsonResponse = [DCTools parseJSON:responseString]; 74 | 75 | //Data values for easy access 76 | int op = [[parsedJsonResponse valueForKey:@"op"] integerValue]; 77 | NSDictionary* d = [parsedJsonResponse valueForKey:@"d"]; 78 | 79 | NSLog(@"Got op code %i", op); 80 | 81 | //revcieved HELLO event 82 | switch(op){ 83 | 84 | case 10: { 85 | 86 | if(weakSelf.shouldResume){ 87 | NSLog(@"Sending Resume with sequence number %i, session ID %@", weakSelf.sequenceNumber, weakSelf.sessionId); 88 | 89 | //RESUME 90 | [weakSelf sendJSON:@{ 91 | @"op":@6, 92 | @"d":@{ 93 | @"token":weakSelf.token, 94 | @"session_id":weakSelf.sessionId, 95 | @"seq":@(weakSelf.sequenceNumber), 96 | } 97 | }]; 98 | 99 | weakSelf.shouldResume = false; 100 | 101 | }else{ 102 | 103 | NSLog(@"Sending Identify"); 104 | 105 | //IDENTIFY 106 | [weakSelf sendJSON:@{ 107 | @"op":@2, 108 | @"d":@{ 109 | @"token":weakSelf.token, 110 | @"properties":@{ @"$browser" : @"peble" }, 111 | @"large_threshold":@"50", 112 | } 113 | }]; 114 | 115 | //Disable ability to identify until reenabled 5 seconds later. 116 | //API only allows once identify every 5 seconds 117 | weakSelf.identifyCooldown = false; 118 | 119 | weakSelf.guilds = NSMutableArray.new; 120 | weakSelf.channels = NSMutableDictionary.new; 121 | weakSelf.loadedUsers = NSMutableDictionary.new; 122 | weakSelf.didRecieveHeartbeatResponse = true; 123 | 124 | int heartbeatInterval = [[d valueForKey:@"heartbeat_interval"] intValue]; 125 | 126 | dispatch_async(dispatch_get_main_queue(), ^{ 127 | 128 | static dispatch_once_t once; 129 | dispatch_once(&once, ^ { 130 | 131 | NSLog(@"Heartbeat is %d seconds", heartbeatInterval/1000); 132 | 133 | //Begin heartbeat cycle if not already begun 134 | [NSTimer scheduledTimerWithTimeInterval:heartbeatInterval/1000 target:weakSelf selector:@selector(sendHeartbeat:) userInfo:nil repeats:YES]; 135 | }); 136 | 137 | //Reenable ability to identify in 5 seconds 138 | weakSelf.cooldownTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:weakSelf selector:@selector(refreshIdentifyCooldown:) userInfo:nil repeats:NO]; 139 | }); 140 | 141 | } 142 | 143 | } 144 | break; 145 | 146 | 147 | //Misc Event 148 | case 0: { 149 | 150 | //Get event type and sequence number 151 | NSString* t = [parsedJsonResponse valueForKey:@"t"]; 152 | weakSelf.sequenceNumber = [[parsedJsonResponse valueForKey:@"s"] integerValue]; 153 | 154 | NSLog(@"Got event %@ with sequence number %i", t, weakSelf.sequenceNumber); 155 | 156 | //recieved READY 157 | if([t isEqualToString:@"READY"]){ 158 | 159 | weakSelf.didAuthenticate = true; 160 | NSLog(@"Did authenticate!"); 161 | 162 | //Grab session id (used for RESUME) and user id 163 | weakSelf.sessionId = [d valueForKey:@"session_id"]; 164 | weakSelf.snowflake = [d valueForKeyPath:@"user.id"]; 165 | 166 | weakSelf.userChannelSettings = NSMutableDictionary.new; 167 | for(NSDictionary* guildSettings in [d valueForKey:@"user_guild_settings"]) 168 | for(NSDictionary* channelSetting in [guildSettings objectForKey:@"channel_overrides"]) 169 | [weakSelf.userChannelSettings setValue:@((bool)[channelSetting valueForKey:@"muted"]) forKey:[channelSetting valueForKey:@"channel_id"]]; 170 | 171 | //Get user DMs and DM groups 172 | //The user's DMs are treated like a guild, where the channels are different DM/groups 173 | DCGuild* privateGuild = DCGuild.new; 174 | privateGuild.name = @"Direct Messages"; 175 | privateGuild.channels = NSMutableArray.new; 176 | 177 | for(NSDictionary* privateChannel in [d valueForKey:@"private_channels"]){ 178 | 179 | DCChannel* newChannel = DCChannel.new; 180 | newChannel.snowflake = [privateChannel valueForKey:@"id"]; 181 | newChannel.lastMessageId = [privateChannel valueForKey:@"last_message_id"]; 182 | newChannel.parentGuild = privateGuild; 183 | newChannel.type = 1; 184 | 185 | NSString* privateChannelName = [privateChannel valueForKey:@"name"]; 186 | 187 | //Some private channels dont have names, check if nil 188 | if(privateChannelName && privateChannelName != (id)NSNull.null){ 189 | newChannel.name = privateChannelName; 190 | }else{ 191 | //If no name, create a name from channel members 192 | NSMutableString* fullChannelName = [@"@" mutableCopy]; 193 | 194 | NSArray* privateChannelMembers = [privateChannel valueForKey:@"recipients"]; 195 | for(NSDictionary* privateChannelMember in privateChannelMembers){ 196 | //add comma between member names 197 | if([privateChannelMembers indexOfObject:privateChannelMember] != 0) 198 | [fullChannelName appendString:@", @"]; 199 | 200 | NSString* memberName = [privateChannelMember valueForKey:@"username"]; 201 | [fullChannelName appendString:memberName]; 202 | 203 | newChannel.name = fullChannelName; 204 | } 205 | } 206 | 207 | [privateGuild.channels addObject:newChannel]; 208 | [weakSelf.channels setObject:newChannel forKey:newChannel.snowflake]; 209 | } 210 | [weakSelf.guilds addObject:privateGuild]; 211 | 212 | 213 | //Get servers (guilds) the user is a member of 214 | for(NSDictionary* jsonGuild in [d valueForKey:@"guilds"]) 215 | [weakSelf.guilds addObject:[DCTools convertJsonGuild:jsonGuild]]; 216 | 217 | 218 | //Read states are recieved in READY payload 219 | //they give a channel ID and the ID of the last read message in that channel 220 | NSArray* readstatesArray = [d valueForKey:@"read_state"]; 221 | 222 | for(NSDictionary* readstate in readstatesArray){ 223 | 224 | NSString* readstateChannelId = [readstate valueForKey:@"id"]; 225 | NSString* readstateMessageId = [readstate valueForKey:@"last_message_id"]; 226 | 227 | //Get the channel with the ID of readStateChannelId 228 | DCChannel* channelOfReadstate = [weakSelf.channels objectForKey:readstateChannelId]; 229 | 230 | channelOfReadstate.lastReadMessageId = readstateMessageId; 231 | [channelOfReadstate checkIfRead]; 232 | } 233 | 234 | dispatch_async(dispatch_get_main_queue(), ^{ 235 | [NSNotificationCenter.defaultCenter postNotificationName:@"READY" object:weakSelf]; 236 | 237 | //Dismiss the 'reconnecting' dialogue box 238 | [weakSelf.alertView dismissWithClickedButtonIndex:0 animated:YES]; 239 | }); 240 | } 241 | 242 | if([t isEqualToString:@"RESUMED"]){ 243 | weakSelf.didAuthenticate = true; 244 | dispatch_async(dispatch_get_main_queue(), ^{ 245 | [weakSelf.alertView dismissWithClickedButtonIndex:0 animated:YES]; 246 | }); 247 | } 248 | 249 | if([t isEqualToString:@"MESSAGE_CREATE"]){ 250 | 251 | NSString* channelIdOfMessage = [d objectForKey:@"channel_id"]; 252 | NSString* messageId = [d objectForKey:@"id"]; 253 | 254 | //Check if a channel is currently being viewed 255 | //and if so, if that channel is the same the message was sent in 256 | if(weakSelf.selectedChannel != nil && [channelIdOfMessage isEqualToString:weakSelf.selectedChannel.snowflake]){ 257 | 258 | dispatch_async(dispatch_get_main_queue(), ^{ 259 | //Send notification with the new message 260 | //will be recieved by DCChatViewController 261 | [NSNotificationCenter.defaultCenter postNotificationName:@"MESSAGE CREATE" object:weakSelf userInfo:d]; 262 | }); 263 | 264 | //Update current channel & read state last message 265 | [weakSelf.selectedChannel setLastMessageId:messageId]; 266 | 267 | //Ack message since we are currently viewing this channel 268 | [weakSelf.selectedChannel ackMessage:messageId]; 269 | }else{ 270 | DCChannel* channelOfMessage = [weakSelf.channels objectForKey:channelIdOfMessage]; 271 | channelOfMessage.lastMessageId = messageId; 272 | 273 | [channelOfMessage checkIfRead]; 274 | 275 | dispatch_async(dispatch_get_main_queue(), ^{ 276 | [NSNotificationCenter.defaultCenter postNotificationName:@"MESSAGE ACK" object:weakSelf]; 277 | }); 278 | } 279 | } 280 | 281 | if([t isEqualToString:@"MESSAGE_ACK"]) 282 | [NSNotificationCenter.defaultCenter postNotificationName:@"MESSAGE ACK" object:weakSelf]; 283 | 284 | if([t isEqualToString:@"MESSAGE_DELETE"]) 285 | dispatch_async(dispatch_get_main_queue(), ^{ 286 | //Send notification with the new message 287 | //will be recieved by DCChatViewController 288 | [NSNotificationCenter.defaultCenter postNotificationName:@"MESSAGE DELETE" object:weakSelf userInfo:d]; 289 | }); 290 | 291 | 292 | if([t isEqualToString:@"GUILD_CREATE"]) 293 | [weakSelf.guilds addObject:[DCTools convertJsonGuild:d]]; 294 | } 295 | break; 296 | 297 | 298 | case 11: { 299 | NSLog(@"Got heartbeat response"); 300 | weakSelf.didRecieveHeartbeatResponse = true; 301 | } 302 | break; 303 | 304 | case 9: 305 | dispatch_async(dispatch_get_main_queue(), ^{ 306 | [weakSelf reconnect]; 307 | }); 308 | break; 309 | } 310 | }]; 311 | 312 | [weakSelf.websocket open]; 313 | } 314 | } 315 | 316 | 317 | - (void)sendResume{ 318 | [self.alertView setTitle:@"Resuming"]; 319 | 320 | self.shouldResume = true; 321 | [self startCommunicator]; 322 | } 323 | 324 | 325 | 326 | - (void)reconnect{ 327 | 328 | NSLog(@"Identify cooldown %s", self.identifyCooldown ? "true" : "false"); 329 | 330 | //Begin new session 331 | [self.websocket close]; 332 | 333 | //If an identify cooldown is in effect, wait for the time needed until sending another IDENTIFY 334 | //if not, send immediately 335 | if(self.identifyCooldown){ 336 | NSLog(@"No cooldown in effect. Authenticating..."); 337 | [self.alertView setTitle:@"Authenticating"]; 338 | [self startCommunicator]; 339 | }else{ 340 | double timeRemaining = self.cooldownTimer.fireDate.timeIntervalSinceNow; 341 | NSLog(@"Cooldown in effect. Time left %f", timeRemaining); 342 | [self.alertView setTitle:@"Waiting for auth cooldown..."]; 343 | [self performSelector:@selector(startCommunicator) withObject:nil afterDelay:timeRemaining + 1]; 344 | } 345 | 346 | self.identifyCooldown = false; 347 | } 348 | 349 | 350 | - (void)sendHeartbeat:(NSTimer *)timer{ 351 | //Check that we've recieved a response since the last heartbeat 352 | if(self.didRecieveHeartbeatResponse){ 353 | [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(checkForRecievedHeartbeat:) userInfo:nil repeats:NO]; 354 | [self sendJSON:@{ @"op": @1, @"d": @(self.sequenceNumber)}]; 355 | NSLog(@"Sent heartbeat"); 356 | [self setDidRecieveHeartbeatResponse:false]; 357 | }else{ 358 | //If we didnt get a response in between heartbeats, we've disconnected from the websocket 359 | //send a RESUME to reconnect 360 | NSLog(@"Did not get heartbeat response, sending RESUME with sequence %i %@", self.sequenceNumber, self.sessionId); 361 | [self sendResume]; 362 | } 363 | } 364 | 365 | - (void)checkForRecievedHeartbeat:(NSTimer *)timer{ 366 | if(!self.didRecieveHeartbeatResponse){ 367 | NSLog(@"Did not get heartbeat response, sending RESUME with sequence %i %@", self.sequenceNumber, self.sessionId); 368 | [self sendResume]; 369 | } 370 | } 371 | 372 | //Once the 5 second identify cooldown is over 373 | - (void)refreshIdentifyCooldown:(NSTimer *)timer{ 374 | self.identifyCooldown = true; 375 | NSLog(@"Authentication cooldown ended"); 376 | } 377 | 378 | - (void)sendJSON:(NSDictionary*)dictionary{ 379 | NSError *writeError = nil; 380 | 381 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:&writeError]; 382 | 383 | NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 384 | [self.websocket sendText:jsonString]; 385 | } 386 | 387 | @end 388 | -------------------------------------------------------------------------------- /Discord Classic/DCSettingsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCSettingsViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 3/18/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCSettingsViewController : UITableViewController 12 | @property (weak, nonatomic) IBOutlet UITextField *tokenInputField; 13 | @end 14 | -------------------------------------------------------------------------------- /Discord Classic/DCSettingsViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCSettingsViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 3/18/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCSettingsViewController.h" 10 | #import "DCServerCommunicator.h" 11 | #import "DCTools.h" 12 | 13 | @implementation DCSettingsViewController 14 | 15 | - (void)viewDidLoad{ 16 | [super viewDidLoad]; 17 | 18 | NSString* token = [NSUserDefaults.standardUserDefaults stringForKey:@"token"]; 19 | 20 | //Show current token in text field if one has previously been entered 21 | if(token) 22 | [self.tokenInputField setText:token]; 23 | } 24 | 25 | - (void)viewWillDisappear:(BOOL)animated{ 26 | [NSUserDefaults.standardUserDefaults setObject:self.tokenInputField.text forKey:@"token"]; 27 | 28 | //Save the entered values and reauthenticate if the token has been changed 29 | if(![DCServerCommunicator.sharedInstance.token isEqual:[NSUserDefaults.standardUserDefaults valueForKey:@"token"]]){ 30 | DCServerCommunicator.sharedInstance.token = self.tokenInputField.text; 31 | [DCServerCommunicator.sharedInstance reconnect]; 32 | 33 | } 34 | } 35 | 36 | - (IBAction)openTutorial:(id)sender { 37 | //Link to video describing how to enter your token 38 | [UIApplication.sharedApplication openURL:[NSURL URLWithString:@"https://www.youtube.com/watch?v=NWB3fGafJwk"]]; 39 | } 40 | 41 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 42 | if(indexPath.row == 1 && indexPath.section == 1){ 43 | [DCTools joinGuild:@"A93uJh3"]; 44 | [self performSegueWithIdentifier:@"Settings to Test Channel" sender:self]; 45 | } 46 | } 47 | 48 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ 49 | if ([segue.identifier isEqualToString:@"Settings to Test Channel"]){ 50 | DCChatViewController *chatViewController = [segue destinationViewController]; 51 | 52 | if ([chatViewController isKindOfClass:DCChatViewController.class]){ 53 | 54 | DCServerCommunicator.sharedInstance.selectedChannel = [DCServerCommunicator.sharedInstance.channels valueForKey:@"422135452657647622"]; 55 | 56 | //Initialize messages 57 | chatViewController.messages = NSMutableArray.new; 58 | 59 | [chatViewController.navigationItem setTitle:@"Testing server #general"]; 60 | 61 | //Populate the message view with the last 50 messages 62 | [chatViewController getMessages:50 beforeMessage:nil]; 63 | 64 | //Chat view is watching the present conversation (auto scroll with new messages) 65 | [chatViewController setViewingPresentTime:YES]; 66 | } 67 | } 68 | } 69 | 70 | @end -------------------------------------------------------------------------------- /Discord Classic/DCTools.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCWebImageOperations.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/17/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DCMessage.h" 11 | #import "DCUser.h" 12 | #import "DCGuild.h" 13 | 14 | @interface DCTools : NSObject 15 | + (void)processImageDataWithURLString:(NSString *)urlString 16 | andBlock:(void (^)(NSData *imageData))processImage; 17 | 18 | + (NSDictionary*)parseJSON:(NSString*)json; 19 | + (void)alert:(NSString*)title withMessage:(NSString*)message; 20 | + (NSData*)checkData:(NSData*)response withError:(NSError*)error; 21 | 22 | + (DCMessage*)convertJsonMessage:(NSDictionary*)jsonMessage; 23 | + (DCGuild *)convertJsonGuild:(NSDictionary*)jsonGuild; 24 | + (DCUser*)convertJsonUser:(NSDictionary*)jsonUser cache:(bool)cache; 25 | 26 | + (void)joinGuild:(NSString*)inviteCode; 27 | @end 28 | -------------------------------------------------------------------------------- /Discord Classic/DCTools.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCWebImageOperations.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/17/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCTools.h" 10 | #import "DCMessage.h" 11 | #import "DCUser.h" 12 | #import "DCServerCommunicator.h" 13 | 14 | //https://discord.gg/X4NSsMC 15 | 16 | @implementation DCTools 17 | + (void)processImageDataWithURLString:(NSString *)urlString 18 | andBlock:(void (^)(NSData *imageData))processImage{ 19 | 20 | NSURL *url = [NSURL URLWithString:urlString]; 21 | 22 | dispatch_queue_t callerQueue = dispatch_get_current_queue(); 23 | dispatch_queue_t downloadQueue = dispatch_queue_create("com.discord_classic.processsmagequeue", NULL); 24 | dispatch_async(downloadQueue, ^{ 25 | NSData* imageData = [NSData dataWithContentsOfURL:url]; 26 | 27 | dispatch_async(callerQueue, ^{ 28 | processImage(imageData); 29 | }); 30 | }); 31 | dispatch_release(downloadQueue); 32 | } 33 | 34 | //Returns a parsed NSDictionary from a json string or nil if something goes wrong 35 | + (NSDictionary*)parseJSON:(NSString*)json{ 36 | NSError *error = nil; 37 | NSData *encodedResponseString = [json dataUsingEncoding:NSUTF8StringEncoding]; 38 | id parsedResponse = [NSJSONSerialization JSONObjectWithData:encodedResponseString options:0 error:&error]; 39 | if([parsedResponse isKindOfClass:NSDictionary.class]){ 40 | return parsedResponse; 41 | } 42 | return nil; 43 | } 44 | 45 | + (void)alert:(NSString*)title withMessage:(NSString*)message{ 46 | dispatch_async(dispatch_get_main_queue(), ^{ 47 | UIAlertView *alert = [UIAlertView.alloc 48 | initWithTitle: title 49 | message: message 50 | delegate: nil 51 | cancelButtonTitle:@"OK" 52 | otherButtonTitles:nil]; 53 | [alert show]; 54 | }); 55 | } 56 | 57 | //Used when making http requests 58 | + (NSData*)checkData:(NSData*)response withError:(NSError*)error{ 59 | if(!response){ 60 | [DCTools alert:error.localizedDescription withMessage:error.localizedRecoverySuggestion]; 61 | return nil; 62 | } 63 | return response; 64 | } 65 | 66 | 67 | 68 | 69 | 70 | 71 | //Converts an NSDictionary created from json representing a user into a DCUser object 72 | //Also keeps the user in DCServerCommunicator.loadedUsers if cache:YES 73 | + (DCUser*)convertJsonUser:(NSDictionary*)jsonUser cache:(bool)cache{ 74 | 75 | DCUser* newUser = DCUser.new; 76 | newUser.username = [jsonUser valueForKey:@"username"]; 77 | newUser.snowflake = [jsonUser valueForKey:@"id"]; 78 | 79 | //Load profile image 80 | NSString* avatarURL = [NSString stringWithFormat:@"https://cdn.discordapp.com/avatars/%@/%@.png", newUser.snowflake, [jsonUser valueForKey:@"avatar"]]; 81 | [DCTools processImageDataWithURLString:avatarURL andBlock:^(NSData *imageData){ 82 | UIImage *retrievedImage = [UIImage imageWithData:imageData]; 83 | 84 | if(retrievedImage != nil){ 85 | newUser.profileImage = retrievedImage; 86 | [NSNotificationCenter.defaultCenter postNotificationName:@"RELOAD CHAT DATA" object:nil]; 87 | } 88 | 89 | }]; 90 | 91 | //Save to DCServerCOmmunicator.loadedUsers 92 | if(cache) 93 | [DCServerCommunicator.sharedInstance.loadedUsers setValue:newUser forKey:newUser.snowflake]; 94 | 95 | return newUser; 96 | } 97 | 98 | 99 | 100 | 101 | 102 | //Converts an NSDictionary created from json representing a message into a message object 103 | + (DCMessage*)convertJsonMessage:(NSDictionary*)jsonMessage{ 104 | DCMessage* newMessage = DCMessage.new; 105 | NSString* authorId = [jsonMessage valueForKeyPath:@"author.id"]; 106 | 107 | if(![DCServerCommunicator.sharedInstance.loadedUsers objectForKey:authorId]) 108 | [DCTools convertJsonUser:[jsonMessage valueForKeyPath:@"author"] cache:true]; 109 | 110 | newMessage.author = [DCServerCommunicator.sharedInstance.loadedUsers valueForKey:authorId]; 111 | 112 | newMessage.content = [jsonMessage valueForKey:@"content"]; 113 | newMessage.snowflake = [jsonMessage valueForKey:@"id"]; 114 | newMessage.embeddedImages = NSMutableArray.new; 115 | newMessage.embeddedImageCount = 0; 116 | 117 | //Load embeded images from both links and attatchments 118 | NSArray* embeds = [jsonMessage objectForKey:@"embeds"]; 119 | if(embeds) 120 | for(NSDictionary* embed in embeds){ 121 | NSString* embedType = [embed valueForKey:@"type"]; 122 | if([embedType isEqualToString:@"image"]){ 123 | newMessage.embeddedImageCount++; 124 | 125 | [DCTools processImageDataWithURLString:[embed valueForKeyPath:@"thumbnail.url"] andBlock:^(NSData *imageData){ 126 | UIImage *retrievedImage = [UIImage imageWithData:imageData]; 127 | 128 | if(retrievedImage != nil){ 129 | [newMessage.embeddedImages addObject:retrievedImage]; 130 | [NSNotificationCenter.defaultCenter postNotificationName:@"RELOAD CHAT DATA" object:nil]; 131 | } 132 | 133 | }]; 134 | } 135 | } 136 | 137 | NSArray* attachments = [jsonMessage objectForKey:@"attachments"]; 138 | if(attachments) 139 | for(NSDictionary* attachment in attachments){ 140 | newMessage.embeddedImageCount++; 141 | 142 | [DCTools processImageDataWithURLString:[attachment valueForKey:@"url"] andBlock:^(NSData *imageData){ 143 | UIImage *retrievedImage = [UIImage imageWithData:imageData]; 144 | 145 | if(retrievedImage != nil){ 146 | [newMessage.embeddedImages addObject:retrievedImage]; 147 | [NSNotificationCenter.defaultCenter postNotificationName:@"RELOAD CHAT DATA" object:nil]; 148 | } 149 | }]; 150 | } 151 | 152 | //Parse in-text mentions into readable @ 153 | NSArray* mentions = [jsonMessage objectForKey:@"mentions"]; 154 | 155 | if(mentions.count){ 156 | 157 | for(NSDictionary* mention in mentions){ 158 | if(![DCServerCommunicator.sharedInstance.loadedUsers valueForKey:[mention valueForKey:@"id"]]){ 159 | [DCTools convertJsonUser:mention cache:true]; 160 | } 161 | } 162 | 163 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\<@(.*?)\\>" options:NSRegularExpressionCaseInsensitive error:NULL]; 164 | 165 | NSTextCheckingResult *embededMention = [regex firstMatchInString:newMessage.content options:0 range:NSMakeRange(0, newMessage.content.length)]; 166 | 167 | while(embededMention){ 168 | 169 | NSCharacterSet *charactersToRemove = [NSCharacterSet.alphanumericCharacterSet invertedSet]; 170 | NSString *mentionSnowflake = [[[newMessage.content substringWithRange:embededMention.range] componentsSeparatedByCharactersInSet:charactersToRemove] componentsJoinedByString:@""]; 171 | 172 | if([mentionSnowflake isEqualToString: DCServerCommunicator.sharedInstance.snowflake]) 173 | newMessage.pingingUser = true; 174 | 175 | DCUser *user = [DCServerCommunicator.sharedInstance.loadedUsers valueForKey:mentionSnowflake]; 176 | 177 | NSString* username = @"@MENTION"; 178 | 179 | if(user) 180 | username = [NSString stringWithFormat:@"@%@", user.username]; 181 | 182 | newMessage.content = [newMessage.content stringByReplacingCharactersInRange:embededMention.range withString:username]; 183 | 184 | embededMention = [regex firstMatchInString:newMessage.content options:0 range:NSMakeRange(0, newMessage.content.length)]; 185 | } 186 | } 187 | 188 | //Calculate height of content to be used when showing messages in a tableview 189 | //contentHeight does NOT include height of the embeded images 190 | float contentWidth = UIScreen.mainScreen.bounds.size.width - 63; 191 | 192 | CGSize authorNameSize = [newMessage.author.username sizeWithFont:[UIFont boldSystemFontOfSize:15] constrainedToSize:CGSizeMake(contentWidth, MAXFLOAT) lineBreakMode:UILineBreakModeWordWrap]; 193 | CGSize contentSize = [newMessage.content sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(contentWidth, MAXFLOAT) lineBreakMode:UILineBreakModeWordWrap]; 194 | 195 | newMessage.contentHeight = authorNameSize.height + contentSize.height + 10; 196 | 197 | return newMessage; 198 | } 199 | 200 | 201 | 202 | 203 | 204 | +(DCGuild *)convertJsonGuild:(NSDictionary*)jsonGuild{ 205 | NSMutableArray* userRoles; 206 | 207 | //Get roles of the current user 208 | for(NSDictionary* member in [jsonGuild objectForKey:@"members"]) 209 | if([[member valueForKeyPath:@"user.id"] isEqualToString:DCServerCommunicator.sharedInstance.snowflake]) 210 | userRoles = [[member valueForKey:@"roles"] mutableCopy]; 211 | 212 | //Get @everyone role 213 | for(NSDictionary* guildRole in [jsonGuild objectForKey:@"roles"]) 214 | if([[guildRole valueForKey:@"name"] isEqualToString:@"@everyone"]) 215 | [userRoles addObject:[guildRole valueForKey:@"id"]]; 216 | 217 | DCGuild* newGuild = DCGuild.new; 218 | newGuild.name = [jsonGuild valueForKey:@"name"]; 219 | newGuild.snowflake = [jsonGuild valueForKey:@"id"]; 220 | newGuild.channels = NSMutableArray.new; 221 | 222 | NSString* iconURL = [NSString stringWithFormat:@"https://cdn.discordapp.com/icons/%@/%@", 223 | newGuild.snowflake, [jsonGuild valueForKey:@"icon"]]; 224 | 225 | [DCTools processImageDataWithURLString:iconURL andBlock:^(NSData *imageData) { 226 | newGuild.icon = [UIImage imageWithData:imageData]; 227 | 228 | dispatch_async(dispatch_get_main_queue(), ^{ 229 | [NSNotificationCenter.defaultCenter postNotificationName:@"RELOAD GUILD LIST" object:DCServerCommunicator.sharedInstance]; 230 | }); 231 | 232 | }]; 233 | 234 | for(NSDictionary* jsonChannel in [jsonGuild valueForKey:@"channels"]){ 235 | 236 | //Make sure jsonChannel is a text cannel 237 | //we dont want to include voice channels in the text channel list 238 | if([[jsonChannel valueForKey:@"type"] isEqual: @0]){ 239 | 240 | //Allow code is used to determine if the user should see the channel in question. 241 | /* 242 | 0 - No overwrides. Channel should be created 243 | 244 | 1 - Hidden by role. Channel should not be created unless another role contradicts (code 2) 245 | 2 - Shown by role. Channel should be created unless hidden by member overwride (code 3) 246 | 247 | 3 - Hidden by member. Channel should not be created 248 | 4 - Shown by member. Channel should be created 249 | 250 | 3 & 4 are mutually exclusive 251 | */ 252 | int allowCode = 0; 253 | 254 | //Calculate permissions 255 | for(NSDictionary* permission in [jsonChannel objectForKey:@"permission_overwrites"]){ 256 | 257 | //Type of permission can either be role or member 258 | NSString* type = [permission valueForKey:@"type"]; 259 | 260 | if([type isEqualToString:@"role"]){ 261 | 262 | //Check if this channel dictates permissions over any roles the user has 263 | if([userRoles containsObject:[permission valueForKey:@"id"]]){ 264 | int deny = [[permission valueForKey:@"deny"] intValue]; 265 | int allow = [[permission valueForKey:@"allow"] intValue]; 266 | 267 | if((deny & 1024) == 1024 && allowCode < 1) 268 | allowCode = 1; 269 | 270 | if(((allow & 1024) == 1024) && allowCode < 2) 271 | allowCode = 2; 272 | } 273 | } 274 | 275 | 276 | if([type isEqualToString:@"member"]){ 277 | 278 | //Check if 279 | NSString* memberId = [permission valueForKey:@"id"]; 280 | if([memberId isEqualToString:DCServerCommunicator.sharedInstance.snowflake]){ 281 | int deny = [[permission valueForKey:@"deny"] intValue]; 282 | int allow = [[permission valueForKey:@"allow"] intValue]; 283 | 284 | if((deny & 1024) == 1024 && allowCode < 3) 285 | allowCode = 3; 286 | 287 | if((allow & 1024) == 1024){ 288 | allowCode = 4; 289 | break; 290 | } 291 | } 292 | } 293 | } 294 | 295 | if(allowCode == 0 || allowCode == 2 || allowCode == 4){ 296 | DCChannel* newChannel = DCChannel.new; 297 | 298 | newChannel.snowflake = [jsonChannel valueForKey:@"id"]; 299 | newChannel.name = [jsonChannel valueForKey:@"name"]; 300 | newChannel.lastMessageId = [jsonChannel valueForKey:@"last_message_id"]; 301 | newChannel.parentGuild = newGuild; 302 | newChannel.type = 0; 303 | 304 | if([DCServerCommunicator.sharedInstance.userChannelSettings objectForKey:newChannel.snowflake]) 305 | newChannel.muted = true; 306 | 307 | //check if channel is muted 308 | 309 | [newGuild.channels addObject:newChannel]; 310 | [DCServerCommunicator.sharedInstance.channels setObject:newChannel forKey:newChannel.snowflake]; 311 | } 312 | } 313 | } 314 | 315 | return newGuild; 316 | } 317 | 318 | 319 | 320 | 321 | 322 | + (void)joinGuild:(NSString*)inviteCode { 323 | NSURL* guildURL = [NSURL URLWithString: [NSString stringWithFormat:@"https://discordapp.com/api/v6/invite/%@", inviteCode]]; 324 | 325 | NSMutableURLRequest *urlRequest=[NSMutableURLRequest requestWithURL:guildURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1]; 326 | 327 | [urlRequest setHTTPMethod:@"POST"]; 328 | 329 | //[urlRequest setHTTPBody:[NSData dataWithBytes:[messageString UTF8String] length:[messageString length]]]; 330 | [urlRequest addValue:DCServerCommunicator.sharedInstance.token forHTTPHeaderField:@"Authorization"]; 331 | [urlRequest addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 332 | 333 | 334 | NSError *error = nil; 335 | NSHTTPURLResponse *responseCode = nil; 336 | 337 | [DCTools checkData:[NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&responseCode error:&error] withError:error]; 338 | } 339 | 340 | @end -------------------------------------------------------------------------------- /Discord Classic/DCUser.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCUser.h 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/17/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCUser : NSObject 12 | @property NSString* snowflake; 13 | @property NSString* username; 14 | @property UIImage* profileImage; 15 | 16 | -(NSString *)description; 17 | @end 18 | -------------------------------------------------------------------------------- /Discord Classic/DCUser.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCUser.m 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/17/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCUser.h" 10 | 11 | @implementation DCUser 12 | 13 | -(NSString *)description{ 14 | return [NSString stringWithFormat:@"[User] Snowflake: %@, Username: %@", self.snowflake, self.username]; 15 | } 16 | @end 17 | -------------------------------------------------------------------------------- /Discord Classic/DCViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/4/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DCServerCommunicator.h" 11 | 12 | @interface DCViewController : UINavigationController 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Discord Classic/DCViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/4/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCViewController.h" 10 | #import "DCServerCommunicator.h" 11 | #import "DCGuildListViewController.h" 12 | 13 | @implementation DCViewController 14 | 15 | - (void)viewDidLoad{ 16 | [super viewDidLoad]; 17 | } 18 | 19 | 20 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{ 21 | return (interfaceOrientation == UIInterfaceOrientationPortrait); 22 | } 23 | 24 | @end -------------------------------------------------------------------------------- /Discord Classic/DCViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1280 5 | 11C25 6 | 1919 7 | 1138.11 8 | 566.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 916 12 | 13 | 14 | IBProxyObject 15 | IBUIView 16 | 17 | 18 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 19 | 20 | 21 | PluginDependencyRecalculationVersion 22 | 23 | 24 | 25 | 26 | IBFilesOwner 27 | IBCocoaTouchFramework 28 | 29 | 30 | IBFirstResponder 31 | IBCocoaTouchFramework 32 | 33 | 34 | 35 | 274 36 | {{0, 20}, {320, 460}} 37 | 38 | 39 | 40 | 3 41 | MQA 42 | 43 | 2 44 | 45 | 46 | 47 | IBCocoaTouchFramework 48 | 49 | 50 | 51 | 52 | 53 | 54 | view 55 | 56 | 57 | 58 | 3 59 | 60 | 61 | 62 | 63 | 64 | 0 65 | 66 | 67 | 68 | 69 | 70 | 1 71 | 72 | 73 | 74 | 75 | -1 76 | 77 | 78 | File's Owner 79 | 80 | 81 | -2 82 | 83 | 84 | 85 | 86 | 87 | 88 | DCViewController 89 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 90 | UIResponder 91 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 92 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 93 | 94 | 95 | 96 | 97 | 98 | 3 99 | 100 | 101 | 102 | 103 | DCViewController 104 | UIViewController 105 | 106 | IBProjectSource 107 | ./Classes/DCViewController.h 108 | 109 | 110 | 111 | 112 | 0 113 | IBCocoaTouchFramework 114 | YES 115 | 3 116 | 916 117 | 118 | 119 | -------------------------------------------------------------------------------- /Discord Classic/DCWelcomeViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCWelcomeViewController.h 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/22/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DCWelcomeViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Discord Classic/DCWelcomeViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DCWelcomeViewController.m 3 | // Discord Classic 4 | // 5 | // Created by Trevir on 11/22/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import "DCWelcomeViewController.h" 10 | 11 | @interface DCWelcomeViewController () 12 | 13 | @end 14 | 15 | @implementation DCWelcomeViewController 16 | 17 | - (void)viewDidLoad{[super viewDidLoad];} 18 | 19 | - (void)didReceiveMemoryWarning{[super didReceiveMemoryWarning];} 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Discord Classic/Discord Classic-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIcons 12 | 13 | CFBundlePrimaryIcon 14 | 15 | CFBundleIconFiles 16 | 17 | Icon@2x.png 18 | Icon.png 19 | 20 | UIPrerenderedIcon 21 | 22 | 23 | 24 | CFBundleIdentifier 25 | com.Trevir.${PRODUCT_NAME:rfc1034identifier} 26 | CFBundleInfoDictionaryVersion 27 | 6.0 28 | CFBundleName 29 | ${PRODUCT_NAME} 30 | CFBundlePackageType 31 | APPL 32 | CFBundleShortVersionString 33 | 1.3 34 | CFBundleSignature 35 | ???? 36 | CFBundleVersion 37 | 1.0 38 | LSRequiresIPhoneOS 39 | 40 | UIMainStoryboardFile 41 | Storyboard 42 | UIMainStoryboardFile~ipad 43 | MainStoryboard_iPad 44 | UIPrerenderedIcon 45 | 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UIStatusBarHidden 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Discord Classic/Discord Classic-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'Discord Classic' target in the 'Discord Classic' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_5_0 8 | #warning "This project uses features only available in iOS SDK 5.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /Discord Classic/TRMalleableFrameView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TRMalleableFrameView.h 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/11/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIView (TRMalleableFrameView) 12 | @property (nonatomic, assign) CGFloat height; 13 | @property (nonatomic, assign) CGFloat width; 14 | @property (nonatomic, assign) CGFloat x; 15 | @property (nonatomic, assign) CGFloat y; 16 | @end 17 | -------------------------------------------------------------------------------- /Discord Classic/TRMalleableFrameView.m: -------------------------------------------------------------------------------- 1 | // 2 | // TRMalleableFrameView.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/11/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | #import "TRMalleableFrameView.h" 9 | @implementation UIView (TRMalleableFrameView) 10 | 11 | - (CGFloat) height { 12 | return self.frame.size.height; 13 | } 14 | 15 | - (CGFloat) width { 16 | return self.frame.size.width; 17 | } 18 | 19 | - (CGFloat) x { 20 | return self.frame.origin.x; 21 | } 22 | 23 | - (CGFloat) y { 24 | return self.frame.origin.y; 25 | } 26 | 27 | - (CGFloat) centerY { 28 | return self.center.y; 29 | } 30 | 31 | - (CGFloat) centerX { 32 | return self.center.x; 33 | } 34 | 35 | - (void) setHeight:(CGFloat) newHeight { 36 | CGRect frame = self.frame; 37 | frame.size.height = newHeight; 38 | self.frame = frame; 39 | } 40 | 41 | - (void) setWidth:(CGFloat) newWidth { 42 | CGRect frame = self.frame; 43 | frame.size.width = newWidth; 44 | self.frame = frame; 45 | } 46 | 47 | - (void) setX:(CGFloat) newX { 48 | CGRect frame = self.frame; 49 | frame.origin.x = newX; 50 | self.frame = frame; 51 | } 52 | 53 | - (void) setY:(CGFloat) newY { 54 | CGRect frame = self.frame; 55 | frame.origin.y = newY; 56 | self.frame = frame; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Discord Classic/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Discord Classic/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Discord Classic 4 | // 5 | // Created by Julian Triveri on 3/2/18. 6 | // Copyright (c) 2018 Julian Triveri. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "DCAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([DCAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/Icon.png -------------------------------------------------------------------------------- /Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/Icon@2x.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Julian Triveri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ###### No longer being maintained. Check iOS-Discord-Raspberry instead 2 | 3 | # Discord Classic 4 | A bare-bones Discord client for iOS 5 and 6. 5 | Not compatible with iPad. 6 | 7 | Report bugs, help test, get update alerts and hang out in the [Discord Server](https://discord.gg/A93uJh3) 8 | 9 | ###### Huge thanks to [Tyler Brasher](https://twitter.com/TyBrasher) for icon design 10 | ![icon](https://github.com/Cellomonster/iOS-Discord-Classic/raw/master/Icon%402x.png) 11 | ![screenshot](https://github.com/Cellomonster/iOS-Discord-Classic/raw/master/Screenshot.png) 12 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/Screenshot.png -------------------------------------------------------------------------------- /UINavigationBarTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/UINavigationBarTexture.png -------------------------------------------------------------------------------- /UINavigationBarTexture@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cellomonster/iOS-Discord-Classic/43381528b69c7d64c9d482e97e89a7f728d07166/UINavigationBarTexture@2x.png -------------------------------------------------------------------------------- /Websocket/NSString+Base64.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Base64.h 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 2/29/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import 27 | 28 | /** 29 | Category for creating base64 encoded strings. 30 | */ 31 | @interface NSString (Base64) 32 | 33 | /** 34 | Base64 encodes the given string. 35 | @param strData The string to encode. 36 | */ 37 | + (NSString *)encodeBase64WithString:(NSString *)strData; 38 | 39 | /** 40 | Base64 encodes the given data. 41 | @param objData The data to encode. 42 | */ 43 | + (NSString *)encodeBase64WithData:(NSData *)objData; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Websocket/NSString+Base64.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Base64.m 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 2/29/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | /** 27 | * Copyright (c) 2010 - 2011, Quasidea Development, LLC 28 | * For more information, please go to http://www.quasidea.com/ 29 | * 30 | * Permission is hereby granted, free of charge, to any person obtaining a copy 31 | * of this software and associated documentation files (the "Software"), to deal 32 | * in the Software without restriction, including without limitation the rights 33 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | * copies of the Software, and to permit persons to whom the Software is 35 | * furnished to do so, subject to the following conditions: 36 | * 37 | * The above copyright notice and this permission notice shall be included in 38 | * all copies or substantial portions of the Software. 39 | * 40 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 46 | * THE SOFTWARE. 47 | */ 48 | 49 | 50 | #import "NSString+Base64.h" 51 | 52 | 53 | static const char _base64EncodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 54 | static const short _base64DecodingTable[256] = { 55 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -2, -1, -1, -2, -2, 56 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 57 | -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63, 58 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -2, -2, -2, 59 | -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 60 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, -2, 61 | -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 62 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2, 63 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 64 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 65 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 66 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 67 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 68 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 69 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 70 | -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 71 | }; 72 | 73 | 74 | @implementation NSString (Base64) 75 | 76 | 77 | + (NSString *)encodeBase64WithString:(NSString *)strData { 78 | return [self encodeBase64WithData:[strData dataUsingEncoding:NSUTF8StringEncoding]]; 79 | } 80 | 81 | /* 82 | Base64 Functions ported from PHP's Core 83 | 84 | +----------------------------------------------------------------------+ 85 | | PHP Version 5 | 86 | +----------------------------------------------------------------------+ 87 | | Copyright (c) 1997-2010 The PHP Group | 88 | +----------------------------------------------------------------------+ 89 | | This source file is subject to version 3.01 of the PHP license, | 90 | | that is bundled with this package in the file LICENSE, and is | 91 | | available through the world-wide-web at the following url: | 92 | | http://www.php.net/license/3_01.txt | 93 | | If you did not receive a copy of the PHP license and are unable to | 94 | | obtain it through the world-wide-web, please send a note to | 95 | | license@php.net so we can mail you a copy immediately. | 96 | +----------------------------------------------------------------------+ 97 | | Author: Jim Winstead | 98 | +----------------------------------------------------------------------+ 99 | */ 100 | 101 | + (NSString *)encodeBase64WithData:(NSData *)objData { 102 | const unsigned char * objRawData = [objData bytes]; 103 | char * objPointer; 104 | char * strResult; 105 | 106 | // Get the Raw Data length and ensure we actually have data 107 | size_t intLength = [objData length]; 108 | if (intLength == 0) return nil; 109 | 110 | // Setup the String-based Result placeholder and pointer within that placeholder 111 | strResult = (char *)calloc(((intLength + 2) / 3) * 4, sizeof(char)); 112 | objPointer = strResult; 113 | 114 | // Iterate through everything 115 | while (intLength > 2) { // keep going until we have less than 24 bits 116 | *objPointer++ = _base64EncodingTable[objRawData[0] >> 2]; 117 | *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)]; 118 | *objPointer++ = _base64EncodingTable[((objRawData[1] & 0x0f) << 2) + (objRawData[2] >> 6)]; 119 | *objPointer++ = _base64EncodingTable[objRawData[2] & 0x3f]; 120 | 121 | // we just handled 3 octets (24 bits) of data 122 | objRawData += 3; 123 | intLength -= 3; 124 | } 125 | 126 | // now deal with the tail end of things 127 | if (intLength != 0) { 128 | *objPointer++ = _base64EncodingTable[objRawData[0] >> 2]; 129 | if (intLength > 1) { 130 | *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)]; 131 | *objPointer++ = _base64EncodingTable[(objRawData[1] & 0x0f) << 2]; 132 | *objPointer++ = '='; 133 | } else { 134 | *objPointer++ = _base64EncodingTable[(objRawData[0] & 0x03) << 4]; 135 | *objPointer++ = '='; 136 | *objPointer++ = '='; 137 | } 138 | } 139 | 140 | NSString *strToReturn = [[NSString alloc] initWithBytesNoCopy:strResult length:objPointer - strResult encoding:NSASCIIStringEncoding freeWhenDone:YES]; 141 | return strToReturn; 142 | } 143 | 144 | @end 145 | -------------------------------------------------------------------------------- /Websocket/WSFrame.h: -------------------------------------------------------------------------------- 1 | // 2 | // WSFrame.h 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 3/22/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import 27 | 28 | typedef enum { 29 | WSWebSocketOpcodeContinuation = 0, 30 | WSWebSocketOpcodeText = 1, 31 | WSWebSocketOpcodeBinary = 2, 32 | WSWebSocketOpcodeClose = 8, 33 | WSWebSocketOpcodePing = 9, 34 | WSWebSocketOpcodePong = 10 35 | }WSWebSocketOpcodeType; 36 | 37 | 38 | /** 39 | WebSocket frame to be send to a server. 40 | */ 41 | @interface WSFrame : NSObject 42 | 43 | /** 44 | The type of the frame. 45 | */ 46 | @property (assign, nonatomic, readonly) WSWebSocketOpcodeType opcode; 47 | 48 | /** 49 | The frame data. 50 | */ 51 | @property (strong, nonatomic, readonly) NSMutableData *data; 52 | 53 | /** 54 | The length of the payload. 55 | */ 56 | @property (assign, nonatomic, readonly) uint64_t payloadLength; 57 | 58 | /** 59 | Yes if the frame is a control frame. 60 | */ 61 | @property (assign, nonatomic, readonly) BOOL isControlFrame; 62 | 63 | /** 64 | Designated initializer. Creates a new frame with the given type and data. 65 | @param opcode The opcode of the message 66 | @param data The payload data to be processed 67 | @param maxSize The maximum size of the frame 68 | */ 69 | - (id)initWithOpcode:(WSWebSocketOpcodeType)opcode data:(NSData *)data maxSize:(NSUInteger)maxSize; 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /Websocket/WSFrame.m: -------------------------------------------------------------------------------- 1 | // 2 | // WSFrame.m 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 3/22/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "WSFrame.h" 27 | #import 28 | 29 | static const NSUInteger WSMaskSize = 4; 30 | 31 | @implementation WSFrame 32 | 33 | @synthesize opcode; 34 | @synthesize data; 35 | @synthesize payloadLength; 36 | 37 | - (BOOL)isControlFrame { 38 | if (opcode & 0b00001000) { 39 | return YES; 40 | } 41 | return NO; 42 | } 43 | 44 | 45 | #pragma mark - Frame construction 46 | 47 | 48 | - (void)constructFrameWithOpcode:(WSWebSocketOpcodeType)anOpcode data:(NSData *)payloadData maxSize:(NSUInteger)maxSize { 49 | opcode = anOpcode; 50 | 51 | uint8_t maskBitAndPayloadLength; 52 | 53 | // Default size: sizeof(opcode) + sizeof(maskBitAndPayloadLength) + sizeof(mask) 54 | NSUInteger sizeWithoutPayload = 6; 55 | 56 | uint64_t totalLength = MIN((payloadData.length + sizeWithoutPayload), maxSize); 57 | 58 | // Calculate and set the frame size and payload length 59 | if (totalLength - sizeWithoutPayload < 126) { 60 | maskBitAndPayloadLength = totalLength - sizeWithoutPayload; 61 | } 62 | else { 63 | totalLength = MIN(totalLength + 2, maxSize); 64 | sizeWithoutPayload += 2; 65 | 66 | if (totalLength - sizeWithoutPayload < 65536) { 67 | maskBitAndPayloadLength = 126; 68 | } 69 | else { 70 | totalLength = MIN(totalLength + 6, maxSize); 71 | maskBitAndPayloadLength = 127; 72 | sizeWithoutPayload += 6; 73 | } 74 | } 75 | 76 | payloadLength = totalLength - sizeWithoutPayload; 77 | 78 | // Set the opcode 79 | uint8_t finBitAndOpcode = anOpcode; 80 | 81 | // Set fin bit 82 | if (payloadLength == payloadData.length) { 83 | finBitAndOpcode |= 0b10000000; 84 | } 85 | 86 | // Create the frame data 87 | data = [[NSMutableData alloc] initWithLength:totalLength]; 88 | uint8_t *frameBytes = (uint8_t *)(data.mutableBytes); 89 | 90 | // Store the opcode 91 | frameBytes[0] = finBitAndOpcode; 92 | 93 | // Set the mask bit 94 | maskBitAndPayloadLength |= 0b10000000; 95 | 96 | // Store mask bit and payload length 97 | frameBytes[1] = maskBitAndPayloadLength; 98 | 99 | if (payloadLength > 65535) { 100 | uint64_t *payloadLength64 = (uint64_t *)(frameBytes + 2); 101 | *payloadLength64 = CFSwapInt64HostToBig(payloadLength); 102 | } 103 | else if (payloadLength > 125) { 104 | uint16_t *payloadLength16 = (uint16_t *)(frameBytes + 2); 105 | *payloadLength16 = CFSwapInt16HostToBig(payloadLength); 106 | } 107 | 108 | // Generate a new mask 109 | uint8_t mask[WSMaskSize]; 110 | SecRandomCopyBytes(kSecRandomDefault, WSMaskSize, mask); 111 | 112 | // Store mask key 113 | uint8_t *mask8 = (uint8_t *)(frameBytes + sizeWithoutPayload - sizeof(mask)); 114 | (void)memcpy(mask8, mask, sizeof(mask)); 115 | 116 | // Store the payload data 117 | frameBytes += sizeWithoutPayload; 118 | (void)memcpy(frameBytes, payloadData.bytes, payloadLength); 119 | 120 | // Mask the payload data 121 | for (int i = 0; i < payloadLength; i++) { 122 | frameBytes[i] ^= mask[i % 4]; 123 | } 124 | } 125 | 126 | 127 | #pragma mark - Public interface 128 | 129 | 130 | - (id)initWithOpcode:(WSWebSocketOpcodeType)anOpcode data:(NSData *)aData maxSize:(NSUInteger)maxSize { 131 | self = [super init]; 132 | if (self) { 133 | [self constructFrameWithOpcode:anOpcode data:aData maxSize:maxSize]; 134 | } 135 | return self; 136 | } 137 | 138 | @end 139 | -------------------------------------------------------------------------------- /Websocket/WSMessage.h: -------------------------------------------------------------------------------- 1 | // 2 | // WSMessage.h 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 3/22/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import 27 | #import "WSFrame.h" 28 | 29 | /** 30 | Message for communicating with a WebSocket server. 31 | */ 32 | @interface WSMessage : NSObject 33 | 34 | /** 35 | The type of the message. 36 | */ 37 | @property (assign, nonatomic) WSWebSocketOpcodeType opcode; 38 | 39 | /** 40 | The message data. 41 | */ 42 | @property (strong, nonatomic) NSData *data; 43 | 44 | /** 45 | The message text. 46 | */ 47 | @property (strong, nonatomic) NSString *text; 48 | 49 | /** 50 | The status code. 51 | */ 52 | @property (assign, nonatomic) NSInteger statusCode; 53 | 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /Websocket/WSMessage.m: -------------------------------------------------------------------------------- 1 | // 2 | // WSMessage.m 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 3/22/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "WSMessage.h" 27 | 28 | @implementation WSMessage 29 | 30 | @synthesize opcode; 31 | @synthesize data; 32 | @synthesize text; 33 | @synthesize statusCode; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /Websocket/WSMessageProcessor.h: -------------------------------------------------------------------------------- 1 | // 2 | // WSMessageProcessor.h 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 3/22/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import 27 | 28 | @class WSFrame; 29 | @class WSMessage; 30 | 31 | /** 32 | This class is responsible for constructing/processing messages. 33 | */ 34 | @interface WSMessageProcessor : NSObject 35 | 36 | /** 37 | Specifies the maximum fragment size to use. 38 | */ 39 | @property (assign, nonatomic) NSUInteger fragmentSize; 40 | 41 | /** 42 | Number of bytes constructed. 43 | */ 44 | @property (assign, nonatomic) NSUInteger bytesConstructed; 45 | 46 | /** 47 | Constructs a message from the received data. 48 | @param data The data to process 49 | */ 50 | - (WSMessage *)constructMessageFromData:(NSData *)data; 51 | 52 | /** 53 | Queues a message to send. 54 | @param message The message to send 55 | */ 56 | - (void)queueMessage:(WSMessage *)message; 57 | 58 | /** 59 | Schedules the next message. 60 | */ 61 | - (void)scheduleNextMessage; 62 | 63 | /** 64 | Processes the current message; 65 | */ 66 | - (void)processMessage; 67 | 68 | /** 69 | Queues a frame to send. 70 | @param frame The frame to send 71 | */ 72 | - (void)queueFrame:(WSFrame *)frame; 73 | 74 | /** 75 | Returns the next frame to send. 76 | */ 77 | - (WSFrame *)nextFrame; 78 | 79 | @end 80 | -------------------------------------------------------------------------------- /Websocket/WSMessageProcessor.m: -------------------------------------------------------------------------------- 1 | // 2 | // WSMessageProcessor.m 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 3/22/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "WSMessageProcessor.h" 27 | 28 | #import "WSFrame.h" 29 | #import "WSMessage.h" 30 | 31 | 32 | @implementation WSMessageProcessor { 33 | NSMutableArray *messagesToSend; 34 | NSMutableArray *framesToSend; 35 | 36 | WSMessage *messageConstructed; 37 | WSMessage *messageProcessed; 38 | NSMutableData *constructedData; 39 | 40 | NSUInteger bytesProcessed; 41 | BOOL isNewMessage; 42 | } 43 | 44 | @synthesize bytesConstructed; 45 | @synthesize fragmentSize; 46 | 47 | 48 | #pragma mark - Object lifecycle 49 | 50 | 51 | - (id)init { 52 | self = [super init]; 53 | if (self) { 54 | messagesToSend = [[NSMutableArray alloc] init]; 55 | framesToSend = [[NSMutableArray alloc] init]; 56 | } 57 | return self; 58 | } 59 | 60 | 61 | #pragma mark - Helper methods 62 | 63 | 64 | - (WSMessage *)messageWithStatusCode:(NSInteger)code text:(NSString *)text { 65 | WSMessage *message = [[WSMessage alloc] init]; 66 | message.opcode = WSWebSocketOpcodeClose; 67 | message.statusCode = code; 68 | message.text = text; 69 | return message; 70 | } 71 | 72 | - (WSMessage *)messageWithStatusCode:(NSInteger)code{ 73 | return [self messageWithStatusCode:code text:nil]; 74 | } 75 | 76 | 77 | #pragma mark - Public interface 78 | 79 | 80 | - (WSMessage *)constructMessageFromData:(NSData *)data { 81 | if (!messageConstructed) { 82 | messageConstructed = [[WSMessage alloc] init]; 83 | constructedData = [[NSMutableData alloc] init]; 84 | isNewMessage = YES; 85 | } 86 | 87 | WSMessage *currentMessage; 88 | 89 | uint8_t *dataBytes = (uint8_t *)[data bytes]; 90 | dataBytes += bytesConstructed; 91 | 92 | NSUInteger frameSize = 2; 93 | uint64_t payloadLength = 0; 94 | 95 | // Frame is not received fully 96 | if (frameSize > data.length - bytesConstructed) { 97 | return nil; 98 | } 99 | 100 | // Mask bit must be clear 101 | if (dataBytes[1] & 0b10000000) { 102 | return [self messageWithStatusCode:1002]; 103 | } 104 | 105 | uint8_t opcode = dataBytes[0] & 0b01111111; 106 | 107 | // Continuation frame received first 108 | if (isNewMessage && opcode == WSWebSocketOpcodeContinuation) { 109 | return [self messageWithStatusCode:1002]; 110 | } 111 | 112 | // Opcode should not be a reserved code 113 | if (opcode != WSWebSocketOpcodeContinuation && opcode != WSWebSocketOpcodeText && opcode != WSWebSocketOpcodeBinary && opcode != WSWebSocketOpcodeClose && opcode != WSWebSocketOpcodePing && opcode != WSWebSocketOpcodePong ) { 114 | return [self messageWithStatusCode:1002]; 115 | } 116 | 117 | // Determine message type 118 | if (opcode == WSWebSocketOpcodeText || opcode == WSWebSocketOpcodeBinary) { 119 | 120 | // Opcode should be continuation 121 | if (!isNewMessage) { 122 | return [self messageWithStatusCode:1002]; 123 | } 124 | 125 | messageConstructed.opcode = opcode; 126 | } 127 | 128 | // Determine payload length 129 | if (dataBytes[1] < 126) { 130 | payloadLength = dataBytes[1]; 131 | } 132 | else if (dataBytes[1] == 126) { 133 | frameSize += 2; 134 | 135 | // Frame is not received fully 136 | if (frameSize > data.length - bytesConstructed) { 137 | return nil; 138 | } 139 | 140 | uint16_t *payloadLength16 = (uint16_t *)(dataBytes + 2); 141 | payloadLength = CFSwapInt16BigToHost(*payloadLength16); 142 | } 143 | else { 144 | frameSize += 8; 145 | 146 | // Frame is not received fully 147 | if (frameSize > data.length - bytesConstructed) { 148 | return nil; 149 | } 150 | 151 | uint64_t *payloadLength64 = (uint64_t *)(dataBytes + 2); 152 | payloadLength = CFSwapInt64BigToHost(*payloadLength64); 153 | } 154 | 155 | // Frame is not received fully 156 | if (payloadLength + frameSize > data.length - bytesConstructed) { 157 | return nil; 158 | } 159 | 160 | uint8_t *payloadData = (uint8_t *)(dataBytes + frameSize); 161 | 162 | // Control frames 163 | if (opcode & 0b00001000) { 164 | 165 | currentMessage = [[WSMessage alloc] init]; 166 | currentMessage.opcode = opcode; 167 | 168 | // Maximum payload length is 125 169 | if (payloadLength > 125) { 170 | return [self messageWithStatusCode:1002]; 171 | } 172 | 173 | // Fin bit must be set 174 | if (~dataBytes[0] & 0b10000000) { 175 | return [self messageWithStatusCode:1002]; 176 | } 177 | 178 | // Close frame 179 | if (opcode == WSWebSocketOpcodeClose) { 180 | uint16_t code = 0; 181 | 182 | if (payloadLength) { 183 | 184 | // Status code must be 2 byte long 185 | if (payloadLength == 1) { 186 | code = 1002; 187 | } 188 | else { 189 | uint16_t *code16 = (uint16_t *)payloadData; 190 | code = CFSwapInt16BigToHost(*code16); 191 | payloadData += 2; 192 | currentMessage.text = [[NSString alloc] initWithBytes:payloadData length:payloadLength - 2 encoding:NSUTF8StringEncoding]; 193 | 194 | // Invalid UTF8 message 195 | if (!currentMessage.text && payloadLength > 2) { 196 | code = 1007; 197 | } 198 | } 199 | } 200 | currentMessage.statusCode = code; 201 | } 202 | 203 | // Ping frame 204 | if (opcode == WSWebSocketOpcodePing) { 205 | currentMessage.data = [NSData dataWithBytes:payloadData length:payloadLength]; 206 | } 207 | 208 | // Pong frame 209 | if (opcode == WSWebSocketOpcodePong) { 210 | currentMessage.data = [NSData dataWithBytes:payloadData length:payloadLength]; 211 | } 212 | } 213 | // Data frames 214 | else { 215 | 216 | // Get payload data 217 | [constructedData appendBytes:payloadData length:payloadLength]; 218 | isNewMessage = NO; 219 | 220 | // In case it was the final fragment 221 | if (dataBytes[0] & 0b10000000) { 222 | 223 | // Text message 224 | if (messageConstructed.opcode == WSWebSocketOpcodeText) { 225 | messageConstructed.text = [[NSString alloc] initWithData:constructedData encoding:NSUTF8StringEncoding]; 226 | 227 | // Invalid UTF8 message 228 | if (!messageConstructed.text && constructedData.length) { 229 | return [self messageWithStatusCode:1007]; 230 | } 231 | } 232 | // Binary message 233 | else if (messageConstructed.opcode == WSWebSocketOpcodeBinary) { 234 | messageConstructed.data = constructedData; 235 | } 236 | 237 | currentMessage = messageConstructed; 238 | messageConstructed = nil; 239 | constructedData = nil; 240 | } 241 | } 242 | 243 | bytesConstructed += (payloadLength + frameSize); 244 | 245 | return currentMessage; 246 | } 247 | 248 | - (void)queueMessage:(WSMessage *)message { 249 | if (message.text) { 250 | message.data = [message.text dataUsingEncoding:NSUTF8StringEncoding]; 251 | message.text = nil; 252 | } 253 | 254 | [messagesToSend addObject:message]; 255 | } 256 | 257 | 258 | - (void)scheduleNextMessage { 259 | if (!messageProcessed && messagesToSend.count) { 260 | messageProcessed = [messagesToSend objectAtIndex:0]; 261 | [messagesToSend removeObjectAtIndex:0]; 262 | } 263 | } 264 | 265 | - (void)processMessage { 266 | // If no message to process then return 267 | if (!messageProcessed) { 268 | return; 269 | } 270 | 271 | uint8_t *dataBytes = (uint8_t *)[messageProcessed.data bytes]; 272 | dataBytes += bytesProcessed; 273 | 274 | uint8_t opcode = messageProcessed.opcode; 275 | 276 | if (bytesProcessed) { 277 | opcode = WSWebSocketOpcodeContinuation; 278 | } 279 | 280 | NSData *data =[NSData dataWithBytesNoCopy:dataBytes length:messageProcessed.data.length - bytesProcessed freeWhenDone:NO]; 281 | 282 | WSFrame *frame = [[WSFrame alloc] initWithOpcode:opcode data:data maxSize:fragmentSize]; 283 | bytesProcessed += frame.payloadLength; 284 | [self queueFrame:frame]; 285 | 286 | // All has been processed 287 | if (messageProcessed.data.length == bytesProcessed) { 288 | messageProcessed = nil; 289 | bytesProcessed = 0; 290 | } 291 | } 292 | 293 | - (void)queueFrame:(WSFrame *)frame { 294 | 295 | // Prioritize ping/pong frames 296 | if (frame.opcode == WSWebSocketOpcodePing || frame.opcode == WSWebSocketOpcodePong) { 297 | 298 | int index = 0; 299 | for (int i = framesToSend.count - 1; i >= 0; i--) { 300 | WSFrame *aFrame = [framesToSend objectAtIndex:i]; 301 | if (aFrame.opcode == frame.opcode) { 302 | index = i + 1; 303 | break; 304 | } 305 | } 306 | [framesToSend insertObject:frame atIndex:index]; 307 | } 308 | else { 309 | [framesToSend addObject:frame]; 310 | } 311 | } 312 | 313 | - (WSFrame *)nextFrame { 314 | if (framesToSend.count) { 315 | WSFrame *nextFrame = [framesToSend objectAtIndex:0]; 316 | [framesToSend removeObjectAtIndex:0]; 317 | return nextFrame; 318 | } 319 | 320 | return nil; 321 | } 322 | 323 | @end 324 | -------------------------------------------------------------------------------- /Websocket/WSWebSocket-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'WSWebSocket' target in the 'WSWebSocket' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /Websocket/WSWebSocket.h: -------------------------------------------------------------------------------- 1 | // 2 | // WSWebSocket.h 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 2/7/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import 27 | 28 | /** 29 | The class is responsible for establishing a websocket connection to the specified URL, sending/receiving messages and closing the connection. 30 | */ 31 | @interface WSWebSocket : NSObject 32 | 33 | /** 34 | Specifies the maximum fragment size to use. Minimum fragment size is 131 bytes. 35 | */ 36 | @property (assign, nonatomic) NSUInteger fragmentSize; 37 | 38 | /** 39 | The host url. 40 | */ 41 | @property (strong, nonatomic, readonly) NSURL *hostURL; 42 | 43 | /** 44 | The protocol selected by the server. 45 | */ 46 | @property (strong, nonatomic, readonly) NSString *selectedProtocol; 47 | 48 | /** 49 | Designated initializer. 50 | @param url The url of the server to connect 51 | @param protocols An optional array of protocol strings 52 | */ 53 | - (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; 54 | 55 | /** 56 | Opens the connection. 57 | */ 58 | - (void)open; 59 | 60 | /** 61 | Closes the connection. 62 | */ 63 | - (void)close; 64 | 65 | /** 66 | Sends the given data. 67 | @param data The data to be sent 68 | */ 69 | - (void)sendData:(NSData *)data; 70 | 71 | /** 72 | Sends the given text. Text should be UTF8 encoded. 73 | @param text The text to be sent. 74 | */ 75 | - (void)sendText:(NSString *)text; 76 | 77 | /** 78 | Sends a ping message with the specified data. 79 | @param data Additional data to be sent as part of the ping message. Maximum data size is 125 bytes. 80 | */ 81 | - (void)sendPingWithData:(NSData *)data; 82 | 83 | /** 84 | Sets a data callback block that will be called whenever a message with data is received. 85 | @param dataCallback The callback block 86 | */ 87 | - (void)setDataCallback:(void (^)(NSData *data))dataCallback; 88 | 89 | /** 90 | Sets a text callback block that will be called whenever a text message is received. 91 | @param textCallback The callback block 92 | */ 93 | - (void)setTextCallback:(void (^)(NSString *text))textCallback; 94 | 95 | /** 96 | Sets a pong callback block that will be called whenever a pong is received. 97 | @param pongCallback The callback block 98 | */ 99 | - (void)setPongCallback:(void (^)(void))pongCallback; 100 | 101 | /** 102 | Sets a callback block that will be called after the connection is closed. 103 | @param closeCallback The callback block 104 | */ 105 | - (void)setCloseCallback:(void (^)(NSUInteger statusCode, NSString *message))closeCallback; 106 | 107 | /** 108 | Sends the given request. 109 | Use this method to send a preconfigured request to handle authentication. 110 | Websocket related header fields are added automatically. Should be used after getting a response callback. 111 | @param request The request to be sent 112 | */ 113 | - (void)sendRequest:(NSURLRequest *)request; 114 | 115 | /** 116 | Sets a callback block that will be called whenever a response is received. 117 | Use this callback to handle authentication and to set any cookies received. 118 | @param responseCallback The callback block 119 | */ 120 | - (void)setResponseCallback:(void (^)(NSHTTPURLResponse *response, NSData *data))responseCallback; 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /Websocket/WSWebSocket.m: -------------------------------------------------------------------------------- 1 | // 2 | // WSWebSocket.m 3 | // WSWebSocket 4 | // 5 | // Created by Andras Koczka on 2/7/12. 6 | // Copyright (c) 2012 Andras Koczka 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "WSWebSocket.h" 27 | #import 28 | #import 29 | 30 | #import "NSString+Base64.h" 31 | #import "WSFrame.h" 32 | #import "WSMessage.h" 33 | #import "WSMessageProcessor.h" 34 | 35 | #define WSSafeCFRelease(obj) if (obj) CFRelease(obj) 36 | 37 | 38 | static const NSUInteger WSNonceSize = 16; 39 | static const NSUInteger WSPort = 80; 40 | static const NSUInteger WSPortSecure = 443; 41 | static NSString *const WSAcceptGUID = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 42 | static NSString *const WSScheme = @"ws"; 43 | static NSString *const WSSchemeSecure = @"wss"; 44 | 45 | static CFStringRef const kWSConnection = CFSTR("Connection"); 46 | static CFStringRef const kWSConnectionValue = CFSTR("Upgrade"); 47 | static CFStringRef const kWSGet = CFSTR("GET"); 48 | static CFStringRef const kWSHost = CFSTR("Host"); 49 | static CFStringRef const kWSHTTP11 = CFSTR("HTTP/1.1"); 50 | static CFStringRef const kWSOrigin = CFSTR("Origin"); 51 | static CFStringRef const kWSUpgrade = CFSTR("Upgrade"); 52 | static CFStringRef const kWSUpgradeValue = CFSTR("websocket"); 53 | static CFStringRef const kWSVersion = CFSTR("13"); 54 | static CFStringRef const kWSContentLength = CFSTR("Content-Length"); 55 | 56 | static CFStringRef const kWSSecWebSocketAccept = CFSTR("Sec-WebSocket-Accept"); 57 | static CFStringRef const kWSSecWebSocketExtensions = CFSTR("Sec-WebSocket-Extensions"); 58 | static CFStringRef const kWSSecWebSocketKey = CFSTR("Sec-WebSocket-Key"); 59 | static CFStringRef const kWSSecWebSocketProtocol = CFSTR("Sec-WebSocket-Protocol"); 60 | static CFStringRef const kWSSecWebSocketVersion = CFSTR("Sec-WebSocket-Version"); 61 | 62 | static const NSUInteger WSHTTPCode101 = 101; 63 | 64 | 65 | typedef enum { 66 | WSWebSocketStateNone = 0, 67 | WSWebSocketStateConnecting = 1, 68 | WSWebSocketStateOpen = 2, 69 | WSWebSocketStateClosing = 3, 70 | WSWebSocketStateClosed = 4 71 | }WSWebSocketStateType; 72 | 73 | 74 | @implementation WSWebSocket { 75 | NSArray *protocols; 76 | 77 | NSInputStream *inputStream; 78 | NSOutputStream *outputStream; 79 | BOOL hasSpaceAvailable; 80 | 81 | NSMutableData *dataReceived; 82 | NSData *dataToSend; 83 | NSInteger bytesSent; 84 | 85 | WSWebSocketStateType state; 86 | NSString *acceptKey; 87 | 88 | NSThread *wsThread; 89 | dispatch_queue_t callbackQueue; 90 | void (^dataCallback)(NSData *data); 91 | void (^textCallback)(NSString *text); 92 | void (^pongCallback)(void); 93 | void (^closeCallback)(NSUInteger statusCode, NSString *message); 94 | void (^responseCallback)(NSHTTPURLResponse *response, NSData *data); 95 | 96 | WSMessageProcessor *messageProcessor; 97 | WSFrame *currentFrame; 98 | uint16_t statusCode; 99 | NSString *closingReason; 100 | } 101 | 102 | 103 | @synthesize fragmentSize; 104 | @synthesize hostURL; 105 | @synthesize selectedProtocol; 106 | 107 | 108 | - (void)setFragmentSize:(NSUInteger)aFragmentSize { 109 | fragmentSize = aFragmentSize; 110 | 111 | if (fragmentSize < 131) { 112 | fragmentSize = 131; 113 | } 114 | 115 | messageProcessor.fragmentSize = fragmentSize; 116 | } 117 | 118 | 119 | #pragma mark - Object lifecycle 120 | 121 | 122 | - (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocolStrings { 123 | self = [super init]; 124 | if (self) { 125 | [self analyzeURL:url]; 126 | hostURL = url; 127 | protocols = protocolStrings; 128 | messageProcessor = [[WSMessageProcessor alloc] init]; 129 | self.fragmentSize = NSUIntegerMax; 130 | callbackQueue = dispatch_queue_create("WebSocket callback queue", DISPATCH_QUEUE_SERIAL); 131 | } 132 | return self; 133 | } 134 | 135 | - (void)dealloc { 136 | dispatch_release(callbackQueue); 137 | } 138 | 139 | 140 | #pragma mark - Callbacks 141 | 142 | 143 | - (void)setDataCallback:(void (^)(NSData *data))aDataCallback { 144 | dataCallback = aDataCallback; 145 | } 146 | 147 | - (void)setTextCallback:(void (^)(NSString *text))aTextCallback { 148 | textCallback = aTextCallback; 149 | } 150 | 151 | - (void)setPongCallback:(void (^)(void))aPongCallback { 152 | pongCallback = aPongCallback; 153 | } 154 | 155 | - (void)setCloseCallback:(void (^)(NSUInteger statusCode, NSString *message))aCloseCallback { 156 | closeCallback = aCloseCallback; 157 | } 158 | 159 | - (void)setResponseCallback:(void (^)(NSHTTPURLResponse *response, NSData *data))aResponseCallback { 160 | responseCallback = aResponseCallback; 161 | } 162 | 163 | 164 | #pragma mark - Helper methods 165 | 166 | 167 | - (void)analyzeURL:(NSURL *)url { 168 | NSAssert(url.scheme, @"Incorrect URL. Unable to determine scheme from URL: %@", url); 169 | NSAssert(url.host, @"Incorrect URL. Unable to determine host from URL: %@", url); 170 | } 171 | 172 | - (NSData *)SHA1DigestOfString:(NSString *)aString { 173 | NSData *data = [aString dataUsingEncoding:NSUTF8StringEncoding]; 174 | 175 | unsigned char digest[CC_SHA1_DIGEST_LENGTH]; 176 | CC_SHA1(data.bytes, data.length, digest); 177 | 178 | return [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH]; 179 | } 180 | 181 | - (NSString *)nonce { 182 | uint8_t nonce[WSNonceSize]; 183 | SecRandomCopyBytes(kSecRandomDefault, WSNonceSize, nonce); 184 | return [NSString encodeBase64WithData:[NSData dataWithBytes:nonce length:WSNonceSize]]; 185 | } 186 | 187 | - (NSString *)acceptKeyFromNonce:(NSString *)nonce { 188 | return [NSString encodeBase64WithData:[self SHA1DigestOfString:[nonce stringByAppendingString:WSAcceptGUID]]]; 189 | } 190 | 191 | 192 | #pragma mark - Data stream 193 | 194 | 195 | - (void)initiateConnection { 196 | CFReadStreamRef readStream; 197 | CFWriteStreamRef writeStream; 198 | NSUInteger port = (hostURL.port) ? hostURL.port.integerValue : ([hostURL.scheme.lowercaseString isEqualToString:WSScheme.lowercaseString]) ? WSPort : WSPortSecure; 199 | 200 | CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)hostURL.host, port, &readStream, &writeStream); 201 | 202 | inputStream = (__bridge_transfer NSInputStream *)readStream; 203 | outputStream = (__bridge_transfer NSOutputStream *)writeStream; 204 | 205 | if ([hostURL.scheme isEqualToString:WSSchemeSecure]) { 206 | [inputStream setProperty:NSStreamSocketSecurityLevelTLSv1 forKey:NSStreamSocketSecurityLevelKey]; 207 | [outputStream setProperty:NSStreamSocketSecurityLevelTLSv1 forKey:NSStreamSocketSecurityLevelKey]; 208 | } 209 | 210 | inputStream.delegate = self; 211 | outputStream.delegate = self; 212 | [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 213 | [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 214 | [inputStream open]; 215 | [outputStream open]; 216 | } 217 | 218 | - (void)closeConnection { 219 | [inputStream close]; 220 | [outputStream close]; 221 | [inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 222 | [outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 223 | inputStream.delegate = nil; 224 | outputStream.delegate = nil; 225 | inputStream = nil; 226 | outputStream = nil; 227 | state = WSWebSocketStateClosed; 228 | 229 | if (closeCallback) { 230 | dispatch_async(callbackQueue, ^{ 231 | closeCallback(statusCode, closingReason); 232 | }); 233 | } 234 | } 235 | 236 | 237 | #pragma mark - Receive data 238 | 239 | 240 | - (BOOL)constructMessage { 241 | 242 | if (!dataReceived.length) { 243 | return NO; 244 | } 245 | 246 | NSUInteger bytesConstructed = messageProcessor.bytesConstructed; 247 | WSMessage *message = [messageProcessor constructMessageFromData:dataReceived]; 248 | 249 | // Close frame 250 | if (message.opcode == WSWebSocketOpcodeClose) { 251 | [self sendCloseControlFrameWithStatusCode:message.statusCode text:message.text]; 252 | } 253 | 254 | // Ping frame 255 | if (message.opcode == WSWebSocketOpcodePing) { 256 | WSFrame *frame = [[WSFrame alloc] initWithOpcode:WSWebSocketOpcodePong data:message.data maxSize:fragmentSize]; 257 | [messageProcessor queueFrame:frame]; 258 | [self sendData]; 259 | } 260 | 261 | // Pong frame 262 | if (message.opcode == WSWebSocketOpcodePong && pongCallback) { 263 | dispatch_async(callbackQueue, ^{ 264 | pongCallback(); 265 | }); 266 | } 267 | 268 | // Text message 269 | if (message.opcode == WSWebSocketOpcodeText && textCallback) { 270 | 271 | // Execute the callback block with the constructed message. 272 | dispatch_async(callbackQueue, ^{ 273 | textCallback(message.text); 274 | }); 275 | } 276 | 277 | // Binary message 278 | else if (message.opcode == WSWebSocketOpcodeBinary && dataCallback) { 279 | 280 | // Execute the callback block with the constructed message. 281 | dispatch_async(callbackQueue, ^{ 282 | dataCallback(message.data); 283 | }); 284 | } 285 | 286 | return bytesConstructed != messageProcessor.bytesConstructed; 287 | } 288 | 289 | - (void)readFromStream { 290 | 291 | if(!dataReceived) { 292 | dataReceived = [[NSMutableData alloc] init]; 293 | } 294 | 295 | NSUInteger bufferSize = fragmentSize; 296 | 297 | // Use a reasonable buffer size 298 | if (fragmentSize == NSUIntegerMax) { 299 | bufferSize = 4096; 300 | } 301 | 302 | uint8_t buffer[bufferSize]; 303 | NSInteger length = bufferSize; 304 | 305 | // Read from the stream 306 | length = [inputStream read:buffer maxLength:bufferSize]; 307 | 308 | // Append the bytes read from the stream 309 | if (length > 0) { 310 | [dataReceived appendBytes:(const void *)buffer length:length]; 311 | } 312 | else { 313 | return; 314 | } 315 | 316 | if (state == WSWebSocketStateConnecting) { 317 | [self processResponse]; 318 | } 319 | 320 | if (state == WSWebSocketStateOpen || state == WSWebSocketStateClosing) { 321 | 322 | // Process all the received data or until a partial received fragment is found 323 | while (messageProcessor.bytesConstructed != dataReceived.length && [self constructMessage]) { 324 | } 325 | 326 | // All data processed 327 | if (messageProcessor.bytesConstructed == dataReceived.length) { 328 | dataReceived = nil; 329 | messageProcessor.bytesConstructed = 0; 330 | } 331 | } 332 | } 333 | 334 | 335 | #pragma mark - Send data 336 | 337 | 338 | - (void)sendCloseControlFrameWithStatusCode:(uint16_t)code text:(NSString *)text { 339 | 340 | if (state != WSWebSocketStateOpen) { 341 | return; 342 | } 343 | 344 | state = WSWebSocketStateClosing; 345 | 346 | NSData *messageData = [text dataUsingEncoding:NSUTF8StringEncoding]; 347 | uint8_t length = (code) ? 2 + messageData.length : 0; 348 | 349 | NSData *frameData; 350 | 351 | // Create the data from the status code and the message 352 | if (length) { 353 | 354 | // Invalid status code 355 | if (code != 1000 && code != 1001 && code != 1002 && code != 1003 && code != 1007 && code != 1008 && code != 1009 && code != 1010 && code != 1011 && code < 3000) { 356 | code = 1002; 357 | } 358 | 359 | uint8_t buffer[length]; 360 | uint8_t *payloadData = (uint8_t *)buffer; 361 | uint16_t *code16 = (uint16_t *)payloadData; 362 | *code16 = CFSwapInt16HostToBig(code); 363 | 364 | statusCode = code; 365 | 366 | if (messageData.length) { 367 | payloadData += 2; 368 | (void)memcpy(payloadData, messageData.bytes, messageData.length); 369 | closingReason = text; 370 | } 371 | 372 | frameData = [NSData dataWithBytes:buffer length:length]; 373 | } 374 | 375 | WSFrame *frame = [[WSFrame alloc] initWithOpcode:WSWebSocketOpcodeClose data:frameData maxSize:fragmentSize]; 376 | [messageProcessor queueFrame:frame]; 377 | [self sendData]; 378 | } 379 | 380 | - (void)sendData { 381 | if (!hasSpaceAvailable) { 382 | return; 383 | } 384 | 385 | if (state == WSWebSocketStateOpen || state == WSWebSocketStateClosing) { 386 | 387 | if (state == WSWebSocketStateOpen) { 388 | [messageProcessor scheduleNextMessage]; 389 | } 390 | 391 | [messageProcessor processMessage]; 392 | 393 | if (!dataToSend) { 394 | currentFrame = [messageProcessor nextFrame]; 395 | dataToSend = currentFrame.data; 396 | } 397 | } 398 | 399 | [self writeToStream]; 400 | } 401 | 402 | - (void)writeToStream { 403 | if (!dataToSend) { 404 | return; 405 | } 406 | 407 | uint8_t *dataBytes = (uint8_t *)[dataToSend bytes]; 408 | dataBytes += bytesSent; 409 | uint64_t length = dataToSend.length - bytesSent; 410 | 411 | hasSpaceAvailable = NO; 412 | length = [outputStream write:dataBytes maxLength:length]; 413 | 414 | if (length > 0) { 415 | bytesSent += length; 416 | 417 | // All data has been sent 418 | if (bytesSent == dataToSend.length) { 419 | bytesSent = 0; 420 | dataToSend = nil; 421 | currentFrame = nil; 422 | } 423 | } 424 | } 425 | 426 | 427 | #pragma mark - NSStreamDelegate 428 | 429 | 430 | - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { 431 | switch (eventCode) { 432 | case NSStreamEventOpenCompleted: 433 | break; 434 | case NSStreamEventHasBytesAvailable: 435 | if (aStream == inputStream) { 436 | [self readFromStream]; 437 | } 438 | break; 439 | case NSStreamEventHasSpaceAvailable: 440 | if (aStream == outputStream) { 441 | hasSpaceAvailable = YES; 442 | [self sendData]; 443 | } 444 | break; 445 | case NSStreamEventErrorOccurred: 446 | statusCode = aStream.streamError.code; 447 | closingReason = [NSString stringWithFormat:@"%@ - %@", aStream.streamError.domain, aStream.streamError.localizedDescription]; 448 | [self closeConnection]; 449 | break; 450 | case NSStreamEventEndEncountered: 451 | [self closeConnection]; 452 | break; 453 | default: 454 | NSLog(@"Unknown event"); 455 | break; 456 | } 457 | } 458 | 459 | 460 | #pragma mark - Handshake 461 | 462 | 463 | - (void)sendOpeningHandshakeWithRequest:(NSURLRequest *)request { 464 | NSString *nonce = [self nonce]; 465 | NSString *hostPort = (hostURL.port) ? [NSString stringWithFormat:@"%@:%@", hostURL.host, hostURL.port] : hostURL.host; 466 | 467 | CFHTTPMessageRef message; 468 | 469 | if (request) { 470 | message = CFHTTPMessageCreateRequest(kCFAllocatorDefault, (__bridge CFStringRef)request.HTTPMethod, (__bridge CFURLRef)request.URL, kWSHTTP11); 471 | 472 | // Copy header fields from request 473 | for (NSString *headerField in request.allHTTPHeaderFields) { 474 | CFHTTPMessageSetHeaderFieldValue(message, (__bridge CFStringRef)headerField, (__bridge CFStringRef)[request.allHTTPHeaderFields objectForKey:headerField]); 475 | } 476 | 477 | // Copy body 478 | if (request.HTTPBody) { 479 | CFHTTPMessageSetBody(message, (__bridge CFDataRef)request.HTTPBody); 480 | } 481 | 482 | } 483 | else { 484 | message = CFHTTPMessageCreateRequest(kCFAllocatorDefault, kWSGet, (__bridge CFURLRef)hostURL, kWSHTTP11); 485 | } 486 | 487 | NSAssert(message, @"Message could not be created from url: %@", hostURL); 488 | 489 | CFHTTPMessageSetHeaderFieldValue(message, kWSHost, (__bridge CFStringRef)hostPort); 490 | CFHTTPMessageSetHeaderFieldValue(message, kWSUpgrade, kWSUpgradeValue); 491 | CFHTTPMessageSetHeaderFieldValue(message, kWSConnection, kWSConnectionValue); 492 | CFHTTPMessageSetHeaderFieldValue(message, kWSSecWebSocketVersion, kWSVersion); 493 | CFHTTPMessageSetHeaderFieldValue(message, kWSSecWebSocketKey, (__bridge CFStringRef)nonce); 494 | 495 | NSMutableString *protocolList; 496 | 497 | // Create a protocol list from the protocol strings 498 | for (NSString *protocolString in protocols) { 499 | if (!protocolList) { 500 | protocolList = [[NSMutableString alloc] initWithString:protocolString]; 501 | } 502 | else { 503 | [protocolList appendFormat:@",%@", protocolString]; 504 | } 505 | } 506 | 507 | // Set the web socket protocol field 508 | if (protocolList.length) { 509 | CFHTTPMessageSetHeaderFieldValue(message, kWSSecWebSocketProtocol, (__bridge CFStringRef)protocolList); 510 | } 511 | 512 | CFDataRef messageData = CFHTTPMessageCopySerializedMessage(message); 513 | dataToSend = (__bridge_transfer NSData *)messageData; 514 | acceptKey = [self acceptKeyFromNonce:nonce]; 515 | 516 | CFRelease(message); 517 | 518 | // NSLog(@"%@", [[NSString alloc] initWithData:dataToSend encoding:NSUTF8StringEncoding]); 519 | } 520 | 521 | - (BOOL)isValidHandshake:(CFHTTPMessageRef)response { 522 | BOOL isValid = YES; 523 | 524 | uint32_t responseStatusCode = CFHTTPMessageGetResponseStatusCode(response); 525 | 526 | if (responseStatusCode != WSHTTPCode101) { 527 | isValid = NO; 528 | } 529 | 530 | CFStringRef upgradeValue = CFHTTPMessageCopyHeaderFieldValue(response, kWSUpgrade); 531 | CFStringRef connectionValue = CFHTTPMessageCopyHeaderFieldValue(response, kWSConnection); 532 | CFStringRef acceptValue = CFHTTPMessageCopyHeaderFieldValue(response, kWSSecWebSocketAccept); 533 | CFStringRef protocolValue = CFHTTPMessageCopyHeaderFieldValue(response, kWSSecWebSocketProtocol); 534 | 535 | if (!upgradeValue || CFStringCompare(upgradeValue, kWSUpgradeValue, kCFCompareCaseInsensitive) != kCFCompareEqualTo) { 536 | isValid = NO; 537 | } 538 | 539 | if (!connectionValue || CFStringCompare(connectionValue, kWSConnectionValue, kCFCompareCaseInsensitive) != kCFCompareEqualTo) { 540 | isValid = NO; 541 | } 542 | 543 | if (!acceptValue || CFStringCompare(acceptValue, (__bridge CFStringRef)acceptKey, kCFCompareCaseInsensitive) != kCFCompareEqualTo) { 544 | isValid = NO; 545 | } 546 | 547 | if (protocolValue) { 548 | selectedProtocol = (__bridge_transfer NSString *)protocolValue; 549 | 550 | // Selected protocol is not in the protocol list - it should fail the connection 551 | if ([protocols indexOfObject:selectedProtocol] == NSNotFound) { 552 | isValid = NO; 553 | [self closeConnection]; 554 | } 555 | } 556 | 557 | WSSafeCFRelease(upgradeValue); 558 | WSSafeCFRelease(connectionValue); 559 | WSSafeCFRelease(acceptValue); 560 | 561 | // CFDataRef messageData = CFHTTPMessageCopySerializedMessage(response); 562 | // NSLog(@"%@", [[NSString alloc] initWithData:(__bridge_transfer NSData*)messageData encoding:NSUTF8StringEncoding]); 563 | 564 | return isValid; 565 | } 566 | 567 | - (void)analyzeResponse:(CFHTTPMessageRef)response { 568 | 569 | if ([self isValidHandshake:response]) { 570 | state = WSWebSocketStateOpen; 571 | [self sendData]; 572 | } 573 | 574 | if (responseCallback) { 575 | 576 | uint32_t responseStatusCode = CFHTTPMessageGetResponseStatusCode(response); 577 | NSDictionary *headerFields = (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(response); 578 | NSHTTPURLResponse *HTTPURLResponse = [[NSHTTPURLResponse alloc] initWithURL:hostURL statusCode:responseStatusCode HTTPVersion:(__bridge NSString *)kWSHTTP11 headerFields:headerFields]; 579 | NSData *data = (__bridge_transfer NSData *)CFHTTPMessageCopyBody(response); 580 | 581 | dispatch_async(callbackQueue, ^{ 582 | responseCallback(HTTPURLResponse, data); 583 | }); 584 | } 585 | else if (state == WSWebSocketStateConnecting) { 586 | [self closeConnection]; 587 | } 588 | } 589 | 590 | - (void)processResponse { 591 | uint8_t *dataBytes = (uint8_t *)[dataReceived bytes]; 592 | 593 | // Find end of the header 594 | for (int i = 0; i < dataReceived.length - 3; i++) { 595 | 596 | // If we have complete header 597 | if (dataBytes[i] == 0x0d && dataBytes[i + 1] == 0x0a && dataBytes[i + 2] == 0x0d && dataBytes[i + 3] == 0x0a) { 598 | 599 | NSUInteger responseLength = i + 4; 600 | BOOL isResponseComplete = YES; 601 | 602 | // Create a CFMessage from the response 603 | CFHTTPMessageRef response = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, NO); 604 | CFHTTPMessageAppendBytes(response, dataBytes, responseLength); 605 | 606 | // Check if it has a body 607 | CFStringRef contentLengthString = CFHTTPMessageCopyHeaderFieldValue(response, kWSContentLength); 608 | 609 | if (contentLengthString) { 610 | 611 | NSUInteger contentLength = CFStringGetIntValue(contentLengthString); 612 | responseLength += contentLength; 613 | 614 | // Not enough data received - body is not complete 615 | if (dataReceived.length < responseLength) { 616 | isResponseComplete = NO; 617 | } 618 | else { 619 | // Add the body data to the response 620 | uint8_t *contentBytes = (uint8_t *)(dataBytes + i + 4); 621 | CFHTTPMessageAppendBytes(response, contentBytes, contentLength); 622 | } 623 | 624 | CFRelease(contentLengthString); 625 | } 626 | 627 | if (isResponseComplete) { 628 | 629 | // Analize it 630 | [self analyzeResponse:response]; 631 | 632 | // Remove the processed handshake data 633 | if (dataReceived.length == responseLength) { 634 | dataReceived = nil; 635 | } 636 | // The remaining bytes are preserved 637 | else { 638 | dataBytes += responseLength; 639 | dataReceived = [[NSMutableData alloc] initWithBytes:dataBytes length:dataReceived.length - responseLength]; 640 | } 641 | } 642 | 643 | CFRelease(response); 644 | 645 | break; 646 | } 647 | } 648 | } 649 | 650 | 651 | #pragma mark - Thread 652 | 653 | 654 | - (void)webSocketThreadLoop { 655 | @autoreleasepool { 656 | while (state != WSWebSocketStateClosed) { 657 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 4.0, NO); 658 | } 659 | } 660 | } 661 | 662 | 663 | #pragma mark - Threaded methods 664 | 665 | 666 | - (void)threadedOpen { 667 | [self initiateConnection]; 668 | [self sendOpeningHandshakeWithRequest:nil]; 669 | } 670 | 671 | - (void)threadedClose { 672 | [self sendCloseControlFrameWithStatusCode:1000 text:nil]; 673 | } 674 | 675 | - (void)threadedSendData:(NSData *)data { 676 | WSMessage *message = [[WSMessage alloc] init]; 677 | 678 | message.opcode = WSWebSocketOpcodeBinary; 679 | message.data = data; 680 | 681 | [messageProcessor queueMessage:message]; 682 | [self sendData]; 683 | } 684 | 685 | - (void)threadedSendText:(NSString *)text { 686 | WSMessage *message = [[WSMessage alloc] init]; 687 | 688 | message.opcode = WSWebSocketOpcodeText; 689 | message.text = text; 690 | 691 | [messageProcessor queueMessage:message]; 692 | [self sendData]; 693 | } 694 | 695 | - (void)threadedSendPingWithData:(NSData *)data { 696 | if (state == WSWebSocketStateConnecting || state == WSWebSocketStateOpen) { 697 | WSFrame *frame = [[WSFrame alloc] initWithOpcode:WSWebSocketOpcodePing data:data maxSize:fragmentSize]; 698 | [messageProcessor queueFrame:frame]; 699 | [self sendData]; 700 | } 701 | } 702 | 703 | - (void)threadedSendRequest:(NSURLRequest *)request { 704 | [self sendOpeningHandshakeWithRequest:request]; 705 | [self sendData]; 706 | } 707 | 708 | 709 | #pragma mark - Public interface 710 | 711 | 712 | - (void)open { 713 | if (state != WSWebSocketStateNone) { 714 | return; 715 | } 716 | 717 | state = WSWebSocketStateConnecting; 718 | 719 | wsThread = [[NSThread alloc] initWithTarget:self selector:@selector(webSocketThreadLoop) object:nil]; 720 | [wsThread start]; 721 | [self performSelector:@selector(threadedOpen) onThread:wsThread withObject:nil waitUntilDone:NO]; 722 | } 723 | 724 | - (void)close { 725 | [self performSelector:@selector(threadedClose) onThread:wsThread withObject:nil waitUntilDone:NO]; 726 | } 727 | 728 | - (void)sendData:(NSData *)data { 729 | if (!data) { 730 | data = [[NSData alloc] init]; 731 | } 732 | 733 | [self performSelector:@selector(threadedSendData:) onThread:wsThread withObject:data waitUntilDone:NO]; 734 | } 735 | 736 | - (void)sendText:(NSString *)text { 737 | if (!text) { 738 | text = @""; 739 | } 740 | 741 | [self performSelector:@selector(threadedSendText:) onThread:wsThread withObject:text waitUntilDone:NO]; 742 | } 743 | 744 | - (void)sendPingWithData:(NSData *)data { 745 | [self performSelector:@selector(threadedSendPingWithData:) onThread:wsThread withObject:data waitUntilDone:NO]; 746 | } 747 | 748 | - (void)sendRequest:(NSURLRequest *)request { 749 | NSAssert(state == WSWebSocketStateConnecting, @"Requests can only be sent during connecting."); 750 | [self performSelector:@selector(threadedSendRequest:) onThread:wsThread withObject:request waitUntilDone:NO]; 751 | } 752 | 753 | @end 754 | --------------------------------------------------------------------------------